mirror of
https://github.com/vitodeploy/vito.git
synced 2025-05-13 19:43:34 +00:00
Setup Inertia (#593)
This commit is contained in:
parent
6eb88c7c6e
commit
38bafd7654
@ -3,12 +3,15 @@ root = true
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
ij_any_block_comment_at_first_column = false
|
||||
|
||||
[*.php]
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
|
@ -13,3 +13,6 @@ sail
|
||||
!*.blade.php
|
||||
!*.sh
|
||||
resources/views/ssh/
|
||||
resources/views/scribe/
|
||||
resources/js/ziggy.js
|
||||
resources/views/mail/*
|
||||
|
24
.prettierrc
24
.prettierrc
@ -1,4 +1,14 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"singleAttributePerLine": false,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"printWidth": 150,
|
||||
"tailwindFunctions": [
|
||||
"clsx",
|
||||
"cn"
|
||||
],
|
||||
"tabWidth": 2,
|
||||
"plugins": [
|
||||
"prettier-plugin-blade",
|
||||
"prettier-plugin-tailwindcss",
|
||||
@ -6,7 +16,9 @@
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.blade.php"],
|
||||
"files": [
|
||||
"*.blade.php"
|
||||
],
|
||||
"options": {
|
||||
"parser": "blade",
|
||||
"printWidth": 120,
|
||||
@ -17,10 +29,18 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.sh"],
|
||||
"files": [
|
||||
"*.sh"
|
||||
],
|
||||
"options": {
|
||||
"parser": "sh"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "**/*.yml",
|
||||
"options": {
|
||||
"tabWidth": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -48,11 +48,6 @@ ## Credits
|
||||
- PHPSecLib
|
||||
- PHPUnit
|
||||
- Tailwindcss
|
||||
- Alpinejs
|
||||
- Livewire
|
||||
- Vite
|
||||
- Prettier
|
||||
- Postcss
|
||||
- FilamentPHP
|
||||
- Mobiledetect
|
||||
- Spatie
|
||||
|
@ -17,6 +17,7 @@
|
||||
use Exception;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@ -29,6 +30,8 @@ class CreateServer
|
||||
*/
|
||||
public function create(User $creator, Project $project, array $input): Server
|
||||
{
|
||||
Validator::make($input, self::rules($project, $input))->validate();
|
||||
|
||||
$server = new Server([
|
||||
'project_id' => $project->id,
|
||||
'user_id' => $creator->id,
|
||||
|
@ -8,6 +8,7 @@
|
||||
use App\Models\User;
|
||||
use App\ServerProviders\ServerProvider as ServerProviderContract;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
@ -20,6 +21,8 @@ class CreateServerProvider
|
||||
*/
|
||||
public function create(User $user, Project $project, array $input): ServerProvider
|
||||
{
|
||||
Validator::make($input, self::rules($input))->validate();
|
||||
|
||||
$provider = self::getProvider($input['provider']);
|
||||
|
||||
try {
|
||||
|
@ -15,7 +15,7 @@
|
||||
* @method static string upload(string $local, string $remote, ?string $owner = null)
|
||||
* @method static string download(string $local, string $remote)
|
||||
* @method static string write(string $path, string $content, string $owner = null)
|
||||
* @method static string assertExecuted(array<int, string>|string $commands)
|
||||
* @method static string assertExecuted(mixed $commands)
|
||||
* @method static string assertExecutedContains(string $command)
|
||||
* @method static string assertFileUploaded(string $toPath, ?string $content = null)
|
||||
* @method static string getUploadedLocalPath()
|
||||
|
88
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Normal file
88
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Spatie\RouteAttributes\Attributes\Get;
|
||||
use Spatie\RouteAttributes\Attributes\Post;
|
||||
|
||||
class AuthenticatedSessionController extends Controller
|
||||
{
|
||||
#[Get('login', name: 'login', middleware: 'guest')]
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
return Inertia::render('auth/login', [
|
||||
'canResetPassword' => Route::has('password.request'),
|
||||
'status' => $request->session()->get('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Post('login', name: 'login', middleware: 'guest')]
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->validate($request, [
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt(['email' => $request->email, 'password' => $request->password], $request->remember)) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => __('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
Session::regenerate();
|
||||
|
||||
return redirect()->intended(route('servers', absolute: false));
|
||||
}
|
||||
|
||||
#[Post('logout', name: 'logout', middleware: 'auth')]
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
protected function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout(request()));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => __('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower(request()->email).'|'.request()->ip());
|
||||
}
|
||||
}
|
43
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Normal file
43
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Spatie\RouteAttributes\Attributes\Get;
|
||||
use Spatie\RouteAttributes\Attributes\Middleware;
|
||||
use Spatie\RouteAttributes\Attributes\Post;
|
||||
use Spatie\RouteAttributes\Attributes\Prefix;
|
||||
|
||||
#[Prefix('confirm-password')]
|
||||
#[Middleware('auth')]
|
||||
class ConfirmablePasswordController extends Controller
|
||||
{
|
||||
#[Get('/', name: 'password.confirm')]
|
||||
public function show(): Response
|
||||
{
|
||||
return Inertia::render('auth/confirm-password');
|
||||
}
|
||||
|
||||
#[Post('/', name: 'password.confirm')]
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if (! Auth::guard('web')->validate([
|
||||
'email' => $request->user()->email,
|
||||
'password' => $request->password,
|
||||
])) {
|
||||
throw ValidationException::withMessages([
|
||||
'password' => __('auth.password'),
|
||||
]);
|
||||
}
|
||||
|
||||
$request->session()->put('auth.password_confirmed_at', time());
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
69
app/Http/Controllers/Auth/NewPasswordController.php
Normal file
69
app/Http/Controllers/Auth/NewPasswordController.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Spatie\RouteAttributes\Attributes\Get;
|
||||
use Spatie\RouteAttributes\Attributes\Middleware;
|
||||
use Spatie\RouteAttributes\Attributes\Post;
|
||||
use Spatie\RouteAttributes\Attributes\Prefix;
|
||||
|
||||
#[Prefix(('reset-password'))]
|
||||
#[Middleware('guest')]
|
||||
class NewPasswordController extends Controller
|
||||
{
|
||||
#[Get('{token}', name: 'password.reset')]
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
return Inertia::render('auth/reset-password', [
|
||||
'email' => $request->email,
|
||||
'token' => $request->route('token'),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Post('/', name: 'password.store')]
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'token' => 'required',
|
||||
'email' => 'required|email',
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
// Here we will attempt to reset the user's password. If it is successful we
|
||||
// will update the password on an actual user model and persist it to the
|
||||
// database. Otherwise we will parse the error and return the response.
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function ($user) use ($request) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($request->password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
// If the password was successfully reset, we will redirect the user back to
|
||||
// the application's home authenticated view. If there is an error we can
|
||||
// redirect them back to where they came from with their error message.
|
||||
if ($status == Password::PasswordReset) {
|
||||
return to_route('login')->with('status', __($status));
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => [__($status)],
|
||||
]);
|
||||
}
|
||||
}
|
41
app/Http/Controllers/Auth/PasswordResetLinkController.php
Normal file
41
app/Http/Controllers/Auth/PasswordResetLinkController.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Spatie\RouteAttributes\Attributes\Get;
|
||||
use Spatie\RouteAttributes\Attributes\Middleware;
|
||||
use Spatie\RouteAttributes\Attributes\Post;
|
||||
use Spatie\RouteAttributes\Attributes\Prefix;
|
||||
|
||||
#[Prefix('forgot-password')]
|
||||
#[Middleware('guest')]
|
||||
class PasswordResetLinkController extends Controller
|
||||
{
|
||||
#[Get('/', name: 'password.request')]
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
return Inertia::render('auth/forgot-password', [
|
||||
'status' => $request->session()->get('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Post('/', name: 'password.email')]
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
]);
|
||||
|
||||
Password::sendResetLink(
|
||||
$request->only('email')
|
||||
);
|
||||
|
||||
return back()->with('status', __('A reset link will be sent if the account exists.'));
|
||||
}
|
||||
}
|
19
app/Http/Controllers/HomeController.php
Normal file
19
app/Http/Controllers/HomeController.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Spatie\RouteAttributes\Attributes\Get;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
#[Get('/', name: 'home')]
|
||||
public function __invoke(): RedirectResponse
|
||||
{
|
||||
if (auth()->check()) {
|
||||
return redirect()->route('servers');
|
||||
}
|
||||
|
||||
return redirect()->route('login');
|
||||
}
|
||||
}
|
70
app/Http/Controllers/ServerController.php
Normal file
70
app/Http/Controllers/ServerController.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Server\CreateServer;
|
||||
use App\Http\Resources\ServerLogResource;
|
||||
use App\Http\Resources\ServerProviderResource;
|
||||
use App\Http\Resources\ServerResource;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerProvider;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Response;
|
||||
use Inertia\ResponseFactory;
|
||||
use Spatie\RouteAttributes\Attributes\Get;
|
||||
use Spatie\RouteAttributes\Attributes\Middleware;
|
||||
use Spatie\RouteAttributes\Attributes\Post;
|
||||
use Spatie\RouteAttributes\Attributes\Prefix;
|
||||
|
||||
#[Prefix('servers')]
|
||||
#[Middleware(['auth', 'has-project'])]
|
||||
class ServerController extends Controller
|
||||
{
|
||||
#[Get('/', name: 'servers')]
|
||||
public function index(): Response|ResponseFactory
|
||||
{
|
||||
$project = user()->currentProject;
|
||||
|
||||
$this->authorize('viewAny', [Server::class, $project]);
|
||||
|
||||
$servers = $project->servers()->simplePaginate(config('web.pagination_size'));
|
||||
|
||||
return inertia('servers/index', [
|
||||
'servers' => ServerResource::collection($servers),
|
||||
'public_key' => __('servers.create.public_key_text', ['public_key' => get_public_key_content()]),
|
||||
'server_providers' => ServerProviderResource::collection(ServerProvider::getByProjectId($project->id)->get()),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Post('/', name: 'servers.store')]
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$project = user()->currentProject;
|
||||
|
||||
$this->authorize('create', [Server::class, $project]);
|
||||
|
||||
$server = app(CreateServer::class)->create(user(), $project, $request->all());
|
||||
|
||||
return redirect()->route('servers.show', ['server' => $server->id]);
|
||||
}
|
||||
|
||||
#[Get('/{server}', name: 'servers.show')]
|
||||
public function show(Server $server): Response|ResponseFactory
|
||||
{
|
||||
$this->authorize('view', $server);
|
||||
|
||||
return inertia('servers/show', [
|
||||
'server' => ServerResource::make($server),
|
||||
'logs' => ServerLogResource::collection($server->logs()->latest()->paginate(config('web.pagination_size'))),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Post('/{server}/switch', name: 'servers.switch')]
|
||||
public function switch(Server $server): RedirectResponse
|
||||
{
|
||||
$this->authorize('view', $server);
|
||||
|
||||
return redirect()->route('servers.show', ['server' => $server->id]);
|
||||
}
|
||||
}
|
22
app/Http/Controllers/ServerLogController.php
Normal file
22
app/Http/Controllers/ServerLogController.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerLog;
|
||||
use Spatie\RouteAttributes\Attributes\Get;
|
||||
use Spatie\RouteAttributes\Attributes\Middleware;
|
||||
use Spatie\RouteAttributes\Attributes\Prefix;
|
||||
|
||||
#[Prefix('servers/{server}/logs')]
|
||||
#[Middleware(['auth', 'has-project'])]
|
||||
class ServerLogController extends Controller
|
||||
{
|
||||
#[Get('/{log}', name: 'logs.show')]
|
||||
public function show(Server $server, ServerLog $log): string
|
||||
{
|
||||
$this->authorize('view', $log);
|
||||
|
||||
return $log->getContent();
|
||||
}
|
||||
}
|
41
app/Http/Controllers/Settings/PasswordController.php
Normal file
41
app/Http/Controllers/Settings/PasswordController.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Spatie\RouteAttributes\Attributes\Get;
|
||||
use Spatie\RouteAttributes\Attributes\Middleware;
|
||||
use Spatie\RouteAttributes\Attributes\Prefix;
|
||||
use Spatie\RouteAttributes\Attributes\Put;
|
||||
|
||||
#[Prefix('settings/password')]
|
||||
#[Middleware(['auth'])]
|
||||
class PasswordController extends Controller
|
||||
{
|
||||
#[Get('/', name: 'password.edit')]
|
||||
public function edit(): Response
|
||||
{
|
||||
return Inertia::render('settings/password');
|
||||
}
|
||||
|
||||
#[Put('/', name: 'password.update')]
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||
]);
|
||||
|
||||
$request->user()->update([
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
73
app/Http/Controllers/Settings/ProfileController.php
Normal file
73
app/Http/Controllers/Settings/ProfileController.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Spatie\RouteAttributes\Attributes\Delete;
|
||||
use Spatie\RouteAttributes\Attributes\Get;
|
||||
use Spatie\RouteAttributes\Attributes\Middleware;
|
||||
use Spatie\RouteAttributes\Attributes\Patch;
|
||||
use Spatie\RouteAttributes\Attributes\Prefix;
|
||||
|
||||
#[Prefix('settings/profile')]
|
||||
#[Middleware(['auth'])]
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
#[Get('/', name: 'profile.edit')]
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
return Inertia::render('settings/profile', [
|
||||
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
|
||||
'status' => $request->session()->get('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Patch('/', name: 'profile.update')]
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class)->ignore(user()->id),
|
||||
],
|
||||
]);
|
||||
$request->user()->fill($request->only('name', 'email'));
|
||||
|
||||
$request->user()->save();
|
||||
|
||||
return to_route('profile.edit');
|
||||
}
|
||||
|
||||
#[Delete('/', name: 'profile.destroy')]
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'password' => ['required', 'current_password'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
27
app/Http/Controllers/Settings/ProjectController.php
Normal file
27
app/Http/Controllers/Settings/ProjectController.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Project;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Spatie\RouteAttributes\Attributes\Middleware;
|
||||
use Spatie\RouteAttributes\Attributes\Post;
|
||||
use Spatie\RouteAttributes\Attributes\Prefix;
|
||||
|
||||
#[Prefix('settings/projects')]
|
||||
#[Middleware(['auth'])]
|
||||
class ProjectController extends Controller
|
||||
{
|
||||
#[Post('switch/{project}', name: 'projects.switch')]
|
||||
public function switch(Project $project): RedirectResponse
|
||||
{
|
||||
$this->authorize('view', $project);
|
||||
|
||||
user()->update([
|
||||
'current_project_id' => $project->id,
|
||||
]);
|
||||
|
||||
return redirect()->route('servers');
|
||||
}
|
||||
}
|
57
app/Http/Controllers/Settings/ServerProviderController.php
Normal file
57
app/Http/Controllers/Settings/ServerProviderController.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Actions\ServerProvider\CreateServerProvider;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ServerProviderResource;
|
||||
use App\Models\ServerProvider;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Spatie\RouteAttributes\Attributes\Get;
|
||||
use Spatie\RouteAttributes\Attributes\Middleware;
|
||||
use Spatie\RouteAttributes\Attributes\Post;
|
||||
use Spatie\RouteAttributes\Attributes\Prefix;
|
||||
|
||||
#[Prefix('settings/server-providers')]
|
||||
#[Middleware(['auth'])]
|
||||
class ServerProviderController extends Controller
|
||||
{
|
||||
public function index(): void {}
|
||||
|
||||
#[Get('/', name: 'server-providers.all')]
|
||||
public function all(): ResourceCollection
|
||||
{
|
||||
$this->authorize('viewAny', ServerProvider::class);
|
||||
|
||||
return ServerProviderResource::collection(ServerProvider::getByProjectId(user()->current_project_id)->get());
|
||||
}
|
||||
|
||||
#[Post('/', name: 'server-providers.store')]
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorize('create', ServerProvider::class);
|
||||
|
||||
app(CreateServerProvider::class)->create(user(), user()->currentProject, $request->all());
|
||||
|
||||
return back()->with('success', 'Server provider created.');
|
||||
}
|
||||
|
||||
#[Get('/{serverProvider}/regions', name: 'server-providers.regions')]
|
||||
public function regions(ServerProvider $serverProvider): JsonResponse
|
||||
{
|
||||
$this->authorize('view', $serverProvider);
|
||||
|
||||
return response()->json($serverProvider->provider()->regions());
|
||||
}
|
||||
|
||||
#[Get('{serverProvider}/regions/{region}/plans', name: 'server-providers.plans')]
|
||||
public function plans(ServerProvider $serverProvider, string $region): JsonResponse
|
||||
{
|
||||
$this->authorize('view', $serverProvider);
|
||||
|
||||
return response()->json($serverProvider->provider()->plans($region));
|
||||
}
|
||||
}
|
@ -36,6 +36,9 @@ class Kernel extends HttpKernel
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\App\Http\Middleware\HandleInertiaRequests::class,
|
||||
\App\Http\Middleware\HandleAppearance::class,
|
||||
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
|
@ -9,10 +9,7 @@
|
||||
|
||||
class CanSeeProjectMiddleware
|
||||
{
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $request->user();
|
||||
|
23
app/Http/Middleware/HandleAppearance.php
Normal file
23
app/Http/Middleware/HandleAppearance.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class HandleAppearance
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
View::share('appearance', $request->cookie('appearance') ?? 'system');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
73
app/Http/Middleware/HandleInertiaRequests.php
Normal file
73
app/Http/Middleware/HandleInertiaRequests.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Http\Resources\ServerResource;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
use Tighten\Ziggy\Ziggy;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
{
|
||||
/**
|
||||
* The root template that's loaded on the first page visit.
|
||||
*
|
||||
* @see https://inertiajs.com/server-side-setup#root-template
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rootView = 'app';
|
||||
|
||||
/**
|
||||
* Determines the current asset version.
|
||||
*
|
||||
* @see https://inertiajs.com/asset-versioning
|
||||
*/
|
||||
public function version(Request $request): ?string
|
||||
{
|
||||
return parent::version($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the props that are shared by default.
|
||||
*
|
||||
* @see https://inertiajs.com/shared-data
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function share(Request $request): array
|
||||
{
|
||||
[$message, $author] = str(Inspiring::quotes()->random())->explode('-');
|
||||
|
||||
/** @var ?User $user */
|
||||
$user = $request->user();
|
||||
|
||||
// servers
|
||||
$servers = [];
|
||||
if ($user && $user->can('viewAny', [Server::class, $user->currentProject])) {
|
||||
$servers = ServerResource::collection($user->currentProject?->servers);
|
||||
}
|
||||
|
||||
return [
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'quote' => ['message' => trim($message), 'author' => trim($author)],
|
||||
'auth' => [
|
||||
'user' => $user,
|
||||
'projects' => $user?->allProjects()->get(),
|
||||
'currentProject' => $user?->currentProject,
|
||||
],
|
||||
'publicKeyText' => __('servers.create.public_key_text', ['public_key' => get_public_key_content()]),
|
||||
'projectServers' => $servers,
|
||||
'configs' => config('core'),
|
||||
'ziggy' => fn (): array => [
|
||||
...(new Ziggy)->toArray(),
|
||||
'location' => $request->url(),
|
||||
],
|
||||
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
||||
];
|
||||
}
|
||||
}
|
@ -2,16 +2,14 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HasProjectMiddleware
|
||||
{
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
/** @var ?User $user */
|
||||
$user = $request->user();
|
||||
@ -21,7 +19,7 @@ public function handle(Request $request, Closure $next)
|
||||
|
||||
if (! $user->currentProject) {
|
||||
if ($user->allProjects()->count() > 0) {
|
||||
/** @var \App\Models\Project $firstProject */
|
||||
/** @var Project $firstProject */
|
||||
$firstProject = $user->allProjects()->first();
|
||||
$user->current_project_id = $firstProject->id;
|
||||
$user->save();
|
||||
|
31
app/Http/Resources/ServerLogResource.php
Normal file
31
app/Http/Resources/ServerLogResource.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\ServerLog;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/** @mixin ServerLog */
|
||||
class ServerLogResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'server_id' => $this->server_id,
|
||||
'site_id' => $this->site_id,
|
||||
'type' => $this->type,
|
||||
'name' => $this->name,
|
||||
'disk' => $this->disk,
|
||||
'is_remote' => $this->is_remote,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
'created_at_by_timezone' => $this->created_at_by_timezone,
|
||||
'updated_at_by_timezone' => $this->updated_at_by_timezone,
|
||||
];
|
||||
}
|
||||
}
|
@ -22,6 +22,8 @@ public function toArray(Request $request): array
|
||||
'provider' => $this->provider,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
'created_at_by_timezone' => $this->created_at_by_timezone,
|
||||
'updated_at_by_timezone' => $this->updated_at_by_timezone,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -38,8 +38,11 @@ public function toArray(Request $request): array
|
||||
'progress_step' => $this->progress_step,
|
||||
'updates' => $this->updates,
|
||||
'last_update_check' => $this->last_update_check,
|
||||
'status_color' => $this->getStatusColor(),
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
'created_at_by_timezone' => $this->created_at_by_timezone,
|
||||
'updated_at_by_timezone' => $this->updated_at_by_timezone,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
use App\SSH\Systemd\Systemd;
|
||||
use App\Support\Testing\SSHFake;
|
||||
use Carbon\Carbon;
|
||||
use Database\Factories\ServerFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
@ -68,7 +69,7 @@
|
||||
*/
|
||||
class Server extends AbstractModel
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\ServerFactory> */
|
||||
/** @use HasFactory<ServerFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
@ -570,4 +571,13 @@ public function download(string $path, string $disk = 'tmp'): void
|
||||
$path
|
||||
);
|
||||
}
|
||||
|
||||
public function getStatusColor(): string
|
||||
{
|
||||
if (isset(self::$statusColors[$this->status])) {
|
||||
return self::$statusColors[$this->status];
|
||||
}
|
||||
|
||||
return 'gray';
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,7 @@
|
||||
use App\Enums\UserRole;
|
||||
use App\Traits\HasTimezoneTimestamps;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Models\Contracts\FilamentUser;
|
||||
use Filament\Panel;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
@ -43,11 +42,11 @@
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
*/
|
||||
class User extends Authenticatable implements FilamentUser
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasApiTokens;
|
||||
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use HasTimezoneTimestamps;
|
||||
@ -70,8 +69,7 @@ class User extends Authenticatable implements FilamentUser
|
||||
'two_factor_secret',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
];
|
||||
protected $appends = [];
|
||||
|
||||
/**
|
||||
* @return HasMany<Server, covariant $this>
|
||||
@ -204,9 +202,4 @@ public function allServers(): Builder
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function canAccessPanel(Panel $panel): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
use App\Models\NotificationChannel;
|
||||
use App\Notifications\NotificationInterface;
|
||||
use App\Web\Pages\Settings\NotificationChannels\Index;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class Discord extends AbstractNotificationChannel
|
||||
@ -39,7 +38,7 @@ public function connect(): bool
|
||||
__('Congratulations! 🎉'),
|
||||
__("You've connected your Discord to :app", ['app' => config('app.name')])."\n".
|
||||
__('Manage your notification channels')."\n".
|
||||
Index::getUrl()
|
||||
'/settings/notification-channels',
|
||||
);
|
||||
|
||||
if (! $connect) {
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
use App\Models\NotificationChannel;
|
||||
use App\Notifications\NotificationInterface;
|
||||
use App\Web\Pages\Settings\NotificationChannels\Index;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class Slack extends AbstractNotificationChannel
|
||||
@ -39,7 +38,7 @@ public function connect(): bool
|
||||
__('Congratulations! 🎉'),
|
||||
__("You've connected your Slack to :app", ['app' => config('app.name')])."\n".
|
||||
__('Manage your notification channels')."\n".
|
||||
Index::getUrl()
|
||||
'/settings/notification-channels',
|
||||
);
|
||||
|
||||
if (! $connect) {
|
||||
|
@ -26,9 +26,9 @@ public function boot(): void
|
||||
ResourceCollection::withoutWrapping();
|
||||
|
||||
// facades
|
||||
$this->app->bind('ssh', fn (): \App\Helpers\SSH => new SSH);
|
||||
$this->app->bind('notifier', fn (): \App\Helpers\Notifier => new Notifier);
|
||||
$this->app->bind('ftp', fn (): \App\Helpers\FTP => new FTP);
|
||||
$this->app->bind('ssh', fn (): SSH => new SSH);
|
||||
$this->app->bind('notifier', fn (): Notifier => new Notifier);
|
||||
$this->app->bind('ftp', fn (): FTP => new FTP);
|
||||
|
||||
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
|
||||
}
|
||||
|
@ -1,123 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Http\Middleware\HasProjectMiddleware;
|
||||
use App\Web\Pages\Login;
|
||||
use App\Web\Pages\Settings\Profile;
|
||||
use App\Web\Pages\Settings\Projects\Widgets\SelectProject;
|
||||
use Exception;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Navigation\MenuItem;
|
||||
use Filament\Panel;
|
||||
use Filament\Support\Assets\Js;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Support\Facades\FilamentAsset;
|
||||
use Filament\Support\Facades\FilamentColor;
|
||||
use Filament\Support\Facades\FilamentView;
|
||||
use Filament\View\PanelsRenderHook;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\Support\Facades\Vite;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
use Livewire\Livewire;
|
||||
|
||||
class WebServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
Filament::registerPanel($this->panel(Panel::make()));
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
FilamentView::registerRenderHook(
|
||||
PanelsRenderHook::SIDEBAR_NAV_START,
|
||||
fn () => Livewire::mount(SelectProject::class)
|
||||
);
|
||||
FilamentView::registerRenderHook(
|
||||
PanelsRenderHook::SIDEBAR_FOOTER,
|
||||
fn () => view('components.app-version')
|
||||
);
|
||||
FilamentAsset::register([
|
||||
Js::make('app', Vite::asset('resources/js/app.js'))->module(),
|
||||
]);
|
||||
FilamentColor::register([
|
||||
'slate' => Color::Slate,
|
||||
'gray' => Color::Zinc,
|
||||
'red' => Color::Red,
|
||||
'orange' => Color::Orange,
|
||||
'amber' => Color::Amber,
|
||||
'yellow' => Color::Yellow,
|
||||
'lime' => Color::Lime,
|
||||
'green' => Color::Green,
|
||||
'emerald' => Color::Emerald,
|
||||
'teal' => Color::Teal,
|
||||
'cyan' => Color::Cyan,
|
||||
'sky' => Color::Sky,
|
||||
'blue' => Color::Blue,
|
||||
'indigo' => Color::Indigo,
|
||||
'violet' => Color::Violet,
|
||||
'purple' => Color::Purple,
|
||||
'fuchsia' => Color::Fuchsia,
|
||||
'pink' => Color::Pink,
|
||||
'rose' => Color::Rose,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
return $panel
|
||||
->default()
|
||||
->id('app')
|
||||
->path('')
|
||||
->passwordReset()
|
||||
->colors([
|
||||
'primary' => Color::Indigo,
|
||||
])
|
||||
->maxContentWidth(MaxWidth::ScreenTwoExtraLarge)
|
||||
->viteTheme('resources/css/filament/app/theme.css')
|
||||
->brandLogo(fn () => view('components.brand'))
|
||||
->brandLogoHeight('30px')
|
||||
->discoverPages(in: app_path('Web/Pages'), for: 'App\\Web\\Pages')
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
AuthenticateSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
])
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
HasProjectMiddleware::class,
|
||||
])
|
||||
->userMenuItems([
|
||||
'profile' => MenuItem::make()
|
||||
->url(fn (): string => Profile\Index::getUrl()),
|
||||
])
|
||||
->login(Login::class)
|
||||
->spa()
|
||||
->globalSearchKeyBindings(['command+k', 'ctrl+k'])
|
||||
->sidebarCollapsibleOnDesktop()
|
||||
->globalSearchFieldKeyBindingSuffix();
|
||||
}
|
||||
}
|
@ -1,12 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Exceptions\SSHError;
|
||||
use Filament\Notifications\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
function generate_public_key(string $privateKeyPath, string $publicKeyPath): void
|
||||
{
|
||||
@ -57,6 +54,10 @@ function convert_time_format(string $string): string
|
||||
|
||||
function get_public_key_content(): string
|
||||
{
|
||||
if (cache()->has('ssh_public_key_content')) {
|
||||
return cache()->get('ssh_public_key_content');
|
||||
}
|
||||
|
||||
if (! file_exists(storage_path(config('core.ssh_public_key_name')))) {
|
||||
Artisan::call('ssh-key:generate --force');
|
||||
}
|
||||
@ -67,46 +68,13 @@ function get_public_key_content(): string
|
||||
return '';
|
||||
}
|
||||
|
||||
return str($content)
|
||||
$content = str($content)
|
||||
->replace("\n", '')
|
||||
->toString();
|
||||
}
|
||||
|
||||
function run_action(object $static, Closure $callback): void
|
||||
{
|
||||
try {
|
||||
$callback();
|
||||
} catch (SSHError $e) {
|
||||
$actions = [];
|
||||
if ($e->getLog() instanceof \App\Models\ServerLog) {
|
||||
$actions[] = Action::make('View Logs')
|
||||
->url(App\Web\Pages\Servers\Logs\Index::getUrl([
|
||||
'server' => $e->getLog()->server_id,
|
||||
]))
|
||||
->openUrlInNewTab();
|
||||
}
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->body($e->getLog()?->getContent(30))
|
||||
->actions($actions)
|
||||
->send();
|
||||
cache()->put('ssh_public_key_content', $content, 60 * 60 * 24);
|
||||
|
||||
if (method_exists($static, 'halt')) {
|
||||
$reflectionMethod = new ReflectionMethod($static, 'halt');
|
||||
$reflectionMethod->invoke($static);
|
||||
}
|
||||
} catch (ValidationException $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
if (method_exists($static, 'halt')) {
|
||||
$reflectionMethod = new ReflectionMethod($static, 'halt');
|
||||
$reflectionMethod->invoke($static);
|
||||
}
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -251,3 +219,11 @@ function format_nginx_config(string $config): string
|
||||
|
||||
return implode("\n", $formattedLines)."\n";
|
||||
}
|
||||
|
||||
function user(): User
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
<?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('components.link');
|
||||
}
|
||||
|
||||
public function toHtml(): View|string
|
||||
{
|
||||
return $this->render()->with([
|
||||
'href' => $this->href,
|
||||
'text' => $this->text,
|
||||
'external' => $this->external,
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Components;
|
||||
|
||||
use App\Models\User;
|
||||
use Filament\Pages\Page as BasePage;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
abstract class Page extends BasePage
|
||||
{
|
||||
protected static string $view = 'components.page';
|
||||
|
||||
protected ?string $live = '5s';
|
||||
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $extraAttributes = [];
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function getExtraAttributes(): array
|
||||
{
|
||||
$attributes = $this->extraAttributes;
|
||||
|
||||
if (! in_array($this->getLive(), [null, '', '0'], true)) {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getUser(): User
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Contracts;
|
||||
|
||||
interface HasSecondSubNav
|
||||
{
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function getSecondSubNavigation(): array;
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Fields;
|
||||
|
||||
use Filament\Forms\Components\Field;
|
||||
|
||||
class AlertField extends Field
|
||||
{
|
||||
protected string $view = 'fields.alert';
|
||||
|
||||
public string $color = 'blue';
|
||||
|
||||
public string $icon = 'heroicon-o-information-circle';
|
||||
|
||||
public string $message = '';
|
||||
|
||||
public function color(string $color): static
|
||||
{
|
||||
$this->color = $color;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function icon(string $icon): static
|
||||
{
|
||||
$this->icon = $icon;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function message(string $message): static
|
||||
{
|
||||
$this->message = $message;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function success(): static
|
||||
{
|
||||
return $this->color('green')->icon('heroicon-o-check-circle');
|
||||
}
|
||||
|
||||
public function warning(): static
|
||||
{
|
||||
return $this->color('yellow')->icon('heroicon-o-exclamation-circle');
|
||||
}
|
||||
|
||||
public function danger(): static
|
||||
{
|
||||
return $this->color('red')->icon('heroicon-o-x-circle');
|
||||
}
|
||||
|
||||
public function info(): static
|
||||
{
|
||||
return $this->color('blue')->icon('heroicon-o-information-circle');
|
||||
}
|
||||
|
||||
public function getColor(): string
|
||||
{
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function getMessage(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Fields;
|
||||
|
||||
use Filament\Forms\Components\Field;
|
||||
|
||||
class CodeEditorField extends Field
|
||||
{
|
||||
protected string $view = 'fields.code-editor';
|
||||
|
||||
public string $lang = '';
|
||||
|
||||
public bool $readonly = false;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'name' => $this->getName(),
|
||||
'lang' => $this->lang,
|
||||
'value' => json_encode($this->getState() ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Fields;
|
||||
|
||||
use Filament\Forms\Components\Field;
|
||||
|
||||
class ProviderField extends Field
|
||||
{
|
||||
protected string $view = 'fields.provider';
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages;
|
||||
|
||||
use App\Models\User;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Http\Responses\Auth\Contracts\LoginResponse;
|
||||
use Filament\Support\Facades\FilamentView;
|
||||
use Filament\View\PanelsRenderHook;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Http\Requests\TwoFactorLoginRequest;
|
||||
|
||||
class Login extends \Filament\Pages\Auth\Login
|
||||
{
|
||||
public function mount(): void
|
||||
{
|
||||
if (Filament::auth()->check()) {
|
||||
redirect()->intended(Filament::getUrl());
|
||||
}
|
||||
|
||||
$this->initTwoFactor();
|
||||
|
||||
$this->form->fill();
|
||||
|
||||
if (config('app.demo')) {
|
||||
$this->form->fill([
|
||||
'email' => 'demo@vitodeploy.com',
|
||||
'password' => 'password',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function logoutAction(): Action
|
||||
{
|
||||
return Action::make('logout')
|
||||
->label('Logout')
|
||||
->color('danger')
|
||||
->link()
|
||||
->action(function (): void {
|
||||
Filament::auth()->logout();
|
||||
|
||||
session()->forget('login.id');
|
||||
|
||||
$this->redirect(Filament::getLoginUrl());
|
||||
});
|
||||
}
|
||||
|
||||
protected function getForms(): array
|
||||
{
|
||||
if (request()->session()->has('login.id')) {
|
||||
return [
|
||||
'form' => $this->form(
|
||||
$this->makeForm()
|
||||
->schema([
|
||||
TextInput::make('code')
|
||||
->label('2FA Code')
|
||||
->autofocus(),
|
||||
TextInput::make('recovery_code')
|
||||
->label('Recovery Code')
|
||||
->autofocus(),
|
||||
])
|
||||
->statePath('data'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return parent::getForms();
|
||||
}
|
||||
|
||||
public function authenticate(): ?LoginResponse
|
||||
{
|
||||
if (request()->session()->has('login.id')) {
|
||||
return $this->confirmTwoFactor();
|
||||
}
|
||||
|
||||
$loginResponse = parent::authenticate();
|
||||
|
||||
/** @var ?User $user */
|
||||
$user = Filament::auth()->getUser();
|
||||
if ($user && $user->two_factor_secret) {
|
||||
Filament::auth()->logout();
|
||||
|
||||
request()->session()->put([
|
||||
'login.id' => $user->getKey(),
|
||||
'login.remember' => $this->data['remember'] ?? false,
|
||||
]);
|
||||
|
||||
$this->redirect(Filament::getLoginUrl());
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $loginResponse;
|
||||
}
|
||||
|
||||
private function confirmTwoFactor(): LoginResponse
|
||||
{
|
||||
$request = TwoFactorLoginRequest::createFrom(request())->merge([
|
||||
'code' => $this->data['code'] ?? null,
|
||||
'recovery_code' => $this->data['recovery_code'] ?? null,
|
||||
]);
|
||||
|
||||
/** @var ?User $user */
|
||||
$user = $request->challengedUser();
|
||||
|
||||
if (! $user) {
|
||||
$this->redirect(Filament::getLoginUrl());
|
||||
|
||||
return app(LoginResponse::class);
|
||||
}
|
||||
|
||||
if ($code = $request->validRecoveryCode()) {
|
||||
$user->replaceRecoveryCode($code);
|
||||
} elseif (! $request->hasValidCode()) {
|
||||
$field = $request->input('recovery_code') ? 'recovery_code' : 'code';
|
||||
|
||||
$this->initTwoFactor();
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'data.'.$field => 'Invalid code!',
|
||||
]);
|
||||
}
|
||||
|
||||
Filament::auth()->login($user, $request->remember());
|
||||
|
||||
return app(LoginResponse::class);
|
||||
}
|
||||
|
||||
protected function getAuthenticateFormAction(): Action
|
||||
{
|
||||
if (request()->session()->has('login.id')) {
|
||||
return Action::make('verify')
|
||||
->label('Verify')
|
||||
->submit('authenticate');
|
||||
}
|
||||
|
||||
return parent::getAuthenticateFormAction();
|
||||
}
|
||||
|
||||
private function initTwoFactor(): void
|
||||
{
|
||||
if (request()->session()->has('login.id')) {
|
||||
FilamentView::registerRenderHook(
|
||||
PanelsRenderHook::AUTH_LOGIN_FORM_BEFORE,
|
||||
fn (): string => Blade::render(
|
||||
<<<BLADE
|
||||
<x-slot name="subheading">{$this->logoutAction()->render()->render()}</x-slot>
|
||||
BLADE
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Scripts;
|
||||
|
||||
use App\Actions\Script\ExecuteScript;
|
||||
use App\Models\Script;
|
||||
use App\Models\Server;
|
||||
use App\Web\Components\Page;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
|
||||
class Executions extends Page
|
||||
{
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static ?string $slug = 'scripts/{script}/executions';
|
||||
|
||||
public Script $script;
|
||||
|
||||
public function getTitle(): string|Htmlable
|
||||
{
|
||||
return $this->script->name.' - Executions';
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('view', $this->script);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\ScriptExecutionsList::class, ['script' => $this->script]],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
$form = [
|
||||
Select::make('server')
|
||||
->options(fn () => auth()->user()?->currentProject?->servers?->pluck('name', 'id') ?? [])
|
||||
->rules(fn (Get $get) => ExecuteScript::rules($get())['server'])
|
||||
->searchable()
|
||||
->reactive(),
|
||||
Select::make('user')
|
||||
->rules(fn (Get $get) => ExecuteScript::rules($get())['user'])
|
||||
->native(false)
|
||||
->options(function (Get $get): array {
|
||||
$users = ['root'];
|
||||
|
||||
/** @var ?Server $server */
|
||||
$server = Server::query()->find($get('server'));
|
||||
if ($server) {
|
||||
$users = $server->getSshUsers();
|
||||
}
|
||||
|
||||
return array_combine($users, $users);
|
||||
}),
|
||||
];
|
||||
|
||||
foreach ($this->script->getVariables() as $variable) {
|
||||
$form[] = TextInput::make('variables.'.$variable)
|
||||
->label($variable)
|
||||
->rules(fn (Get $get) => ExecuteScript::rules($get())['variables.*']);
|
||||
}
|
||||
|
||||
return [
|
||||
Action::make('execute')
|
||||
->icon('heroicon-o-bolt')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->form($form)
|
||||
->action(function (array $data): void {
|
||||
app(ExecuteScript::class)->execute($this->script, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Scripts;
|
||||
|
||||
use App\Actions\Script\CreateScript;
|
||||
use App\Models\Script;
|
||||
use App\Models\User;
|
||||
use App\Web\Components\Page;
|
||||
use App\Web\Fields\CodeEditorField;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $slug = 'scripts';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-bolt';
|
||||
|
||||
protected static ?int $navigationSort = 2;
|
||||
|
||||
protected static ?string $title = 'Scripts';
|
||||
|
||||
public static function getNavigationItemActiveRoutePattern(): string
|
||||
{
|
||||
return static::getRouteName().'*';
|
||||
}
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()?->can('viewAny', Script::class) ?? false;
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\ScriptsList::class],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return [
|
||||
Action::make('read-the-docs')
|
||||
->label('Read the Docs')
|
||||
->icon('heroicon-o-document-text')
|
||||
->color('gray')
|
||||
->url('https://vitodeploy.com/scripts')
|
||||
->openUrlInNewTab(),
|
||||
Action::make('create')
|
||||
->label('Create a Script')
|
||||
->icon('heroicon-o-plus')
|
||||
->authorize('create', Script::class)
|
||||
->modalWidth(MaxWidth::ThreeExtraLarge)
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->rules(CreateScript::rules()['name']),
|
||||
CodeEditorField::make('content')
|
||||
->rules(CreateScript::rules()['content'])
|
||||
->helperText('You can use variables like ${VARIABLE_NAME} in the script. The variables will be asked when executing the script'),
|
||||
Checkbox::make('global')
|
||||
->label('Is Global (Accessible in all projects)'),
|
||||
])
|
||||
->modalSubmitActionLabel('Create')
|
||||
->action(function (array $data) use ($user): void {
|
||||
run_action($this, function () use ($data, $user): void {
|
||||
app(CreateScript::class)->create($user, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Scripts\Widgets;
|
||||
|
||||
use App\Models\Script;
|
||||
use App\Models\ScriptExecution;
|
||||
use App\Web\Pages\Servers\View;
|
||||
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;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
class ScriptExecutionsList extends Widget
|
||||
{
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
public Script $script;
|
||||
|
||||
/**
|
||||
* @return Builder<ScriptExecution>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return ScriptExecution::query()->where('script_id', $this->script->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<ScriptExecution> $query
|
||||
* @return Builder<ScriptExecution>
|
||||
*/
|
||||
protected function applyDefaultSortingToTableQuery(Builder $query): Builder
|
||||
{
|
||||
return $query->latest('created_at');
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('server')
|
||||
->formatStateUsing(fn (ScriptExecution $record) => $record->getServer()->name ?? 'Unknown')
|
||||
->url(function (ScriptExecution $record): ?string {
|
||||
$server = $record->getServer();
|
||||
|
||||
return $server instanceof \App\Models\Server ? View::getUrl(['server' => $server]) : null;
|
||||
})
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('Executed At')
|
||||
->formatStateUsing(fn (ScriptExecution $record) => $record->created_at_by_timezone)
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (ScriptExecution $record) => ScriptExecution::$statusColors[$record->status])
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->heading(null)
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->actions([
|
||||
Action::make('logs')
|
||||
->hiddenLabel()
|
||||
->tooltip('Logs')
|
||||
->icon('heroicon-o-eye')
|
||||
->authorize(fn (ScriptExecution $record) => auth()->user()?->can('view', $record->serverLog))
|
||||
->modalHeading('View Log')
|
||||
->modalContent(fn (ScriptExecution $record) => view('components.console-view', [
|
||||
'slot' => $record->serverLog?->getContent(),
|
||||
'attributes' => new ComponentAttributeBag,
|
||||
]))
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelActionLabel('Close'),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Scripts\Widgets;
|
||||
|
||||
use App\Actions\Script\EditScript;
|
||||
use App\Models\Script;
|
||||
use App\Models\User;
|
||||
use App\Web\Fields\CodeEditorField;
|
||||
use App\Web\Pages\Scripts\Executions;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ScriptsList extends Widget
|
||||
{
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<Script>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return Script::getByProjectId($user->current_project_id, $user->id);
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('id')
|
||||
->label('Global')
|
||||
->badge()
|
||||
->color(fn ($record): string => $record->project_id ? 'gray' : 'success')
|
||||
->formatStateUsing(fn (Script $record): string => $record->project_id ? 'No' : 'Yes'),
|
||||
TextColumn::make('created_at')
|
||||
->label('Created At')
|
||||
->formatStateUsing(fn (Script $record) => $record->created_at_by_timezone)
|
||||
->searchable()
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $table
|
||||
->heading(null)
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->recordUrl(fn (Script $record): string => Executions::getUrl(['script' => $record]))
|
||||
->actions([
|
||||
EditAction::make('edit')
|
||||
->label('Edit')
|
||||
->modalHeading('Edit Script')
|
||||
->mutateRecordDataUsing(fn (array $data, Script $record): array => [
|
||||
'name' => $record->name,
|
||||
'content' => $record->content,
|
||||
'global' => $record->project_id === null,
|
||||
])
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->rules(EditScript::rules()['name']),
|
||||
CodeEditorField::make('content')
|
||||
->rules(EditScript::rules()['content'])
|
||||
->helperText('You can use variables like ${VARIABLE_NAME} in the script. The variables will be asked when executing the script'),
|
||||
Checkbox::make('global')
|
||||
->label('Is Global (Accessible in all projects)'),
|
||||
])
|
||||
->authorize(fn (Script $record) => $user->can('update', $record))
|
||||
->using(function (array $data, Script $record) use ($user): void {
|
||||
app(EditScript::class)->edit($record, $user, $data);
|
||||
$this->dispatch('$refresh');
|
||||
})
|
||||
->modalWidth(MaxWidth::ThreeExtraLarge),
|
||||
DeleteAction::make('delete')
|
||||
->label('Delete')
|
||||
->modalHeading('Delete Script')
|
||||
->authorize(fn (Script $record) => $user->can('delete', $record))
|
||||
->using(function (array $data, Script $record): void {
|
||||
$record->delete();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Console;
|
||||
|
||||
use App\Web\Pages\Servers\Page;
|
||||
use Filament\Actions\Action;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected ?string $live = '';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = [];
|
||||
|
||||
protected static ?string $slug = 'servers/{server}/console';
|
||||
|
||||
protected static ?string $title = 'Headless Console';
|
||||
|
||||
protected static ?string $navigationLabel = 'Console';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('manage', $this->server);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\Console::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('read-the-docs')
|
||||
->label('Read the Docs')
|
||||
->icon('heroicon-o-document-text')
|
||||
->color('gray')
|
||||
->url('https://vitodeploy.com/servers/console.html')
|
||||
->openUrlInNewTab(),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Console\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
class Console extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('components.console', [
|
||||
'server' => $this->server,
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Console\Widgets;
|
||||
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Component;
|
||||
|
||||
class StopCommand extends Component implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
public function stop(): void
|
||||
{
|
||||
Cache::put('console', 0);
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return <<<'BLADE'
|
||||
<div>
|
||||
<x-filament::icon
|
||||
icon="heroicon-o-stop"
|
||||
wire:click="stop"
|
||||
class="h-5 w-5 text-danger-400 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
BLADE;
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\CronJobs;
|
||||
|
||||
use App\Actions\CronJob\CreateCronJob;
|
||||
use App\Models\CronJob;
|
||||
use App\Web\Pages\Servers\Page;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/cronjobs';
|
||||
|
||||
protected static ?string $title = 'Cron Jobs';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [CronJob::class, $this->server]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\CronJobsList::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
$users = $this->server->getSshUsers();
|
||||
|
||||
return [
|
||||
Action::make('read-the-docs')
|
||||
->label('Read the Docs')
|
||||
->icon('heroicon-o-document-text')
|
||||
->color('gray')
|
||||
->url('https://vitodeploy.com/servers/cronjobs')
|
||||
->openUrlInNewTab(),
|
||||
Action::make('create')
|
||||
->authorize(fn () => auth()->user()?->can('create', [CronJob::class, $this->server]))
|
||||
->icon('heroicon-o-plus')
|
||||
->modalWidth(MaxWidth::ExtraLarge)
|
||||
->form([
|
||||
TextInput::make('command')
|
||||
->rules(fn (callable $get) => CreateCronJob::rules($get(), $this->server)['command'])
|
||||
->helperText(fn () => view('components.link', [
|
||||
'href' => 'https://vitodeploy.com/servers/cronjobs',
|
||||
'external' => true,
|
||||
'text' => 'How the command should look like?',
|
||||
])),
|
||||
Select::make('user')
|
||||
->rules(fn (callable $get) => CreateCronJob::rules($get(), $this->server)['user'])
|
||||
->options(array_combine($users, $users)),
|
||||
Select::make('frequency')
|
||||
->options(config('core.cronjob_intervals'))
|
||||
->reactive()
|
||||
->rules(fn (callable $get) => CreateCronJob::rules($get(), $this->server)['frequency']),
|
||||
TextInput::make('custom')
|
||||
->label('Custom Frequency (Cron)')
|
||||
->rules(fn (callable $get) => CreateCronJob::rules($get(), $this->server)['custom'])
|
||||
->visible(fn (callable $get): bool => $get('frequency') === 'custom')
|
||||
->placeholder('0 * * * *'),
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
app(CreateCronJob::class)->create($this->server, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Cron Job created!')
|
||||
->send();
|
||||
});
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\CronJobs\Widgets;
|
||||
|
||||
use App\Actions\CronJob\DeleteCronJob;
|
||||
use App\Actions\CronJob\DisableCronJob;
|
||||
use App\Actions\CronJob\EnableCronJob;
|
||||
use App\Models\CronJob;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Exception;
|
||||
use Filament\Notifications\Notification;
|
||||
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 CronJobsList extends Widget
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<CronJob>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return CronJob::query()->where('server_id', $this->server->id);
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('command')
|
||||
->limit(40)
|
||||
->tooltip(fn (CronJob $cronJob) => $cronJob->command)
|
||||
->searchable()
|
||||
->copyable(),
|
||||
TextColumn::make('frequency')
|
||||
->formatStateUsing(fn (CronJob $cronJob): string => $cronJob->frequencyLabel())
|
||||
->searchable()
|
||||
->sortable()
|
||||
->copyable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (CronJob $record) => CronJob::$statusColors[$record->status])
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->formatStateUsing(fn (CronJob $cronJob) => $cronJob->created_at_by_timezone)
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $table
|
||||
->heading(null)
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->actions([
|
||||
Action::make('enable')
|
||||
->hiddenLabel()
|
||||
->tooltip('Enable')
|
||||
->icon('heroicon-o-play')
|
||||
->requiresConfirmation()
|
||||
->authorize(fn (CronJob $record) => $user->can('update', [$record, $this->server]))
|
||||
->visible(fn (CronJob $record): bool => $record->isDisabled())
|
||||
->action(function (CronJob $record): void {
|
||||
run_action($this, function () use ($record): void {
|
||||
app(EnableCronJob::class)->enable($this->server, $record);
|
||||
});
|
||||
}),
|
||||
Action::make('disable')
|
||||
->hiddenLabel()
|
||||
->tooltip('Disable')
|
||||
->icon('heroicon-o-stop')
|
||||
->requiresConfirmation()
|
||||
->authorize(fn (CronJob $record) => $user->can('update', [$record, $this->server]))
|
||||
->visible(fn (CronJob $record): bool => $record->isEnabled())
|
||||
->action(function (CronJob $record): void {
|
||||
run_action($this, function () use ($record): void {
|
||||
app(DisableCronJob::class)->disable($this->server, $record);
|
||||
});
|
||||
}),
|
||||
Action::make('delete')
|
||||
->icon('heroicon-o-trash')
|
||||
->tooltip('Delete')
|
||||
->color('danger')
|
||||
->hiddenLabel()
|
||||
->requiresConfirmation()
|
||||
->authorize(fn (CronJob $record) => $user->can('delete', $record))
|
||||
->action(function (CronJob $record): void {
|
||||
try {
|
||||
app(DeleteCronJob::class)->delete($this->server, $record);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Databases;
|
||||
|
||||
use App\Actions\Database\ManageBackup;
|
||||
use App\Models\Backup;
|
||||
use App\Models\StorageProvider;
|
||||
use App\Models\User;
|
||||
use App\Web\Contracts\HasSecondSubNav;
|
||||
use App\Web\Pages\Servers\Page;
|
||||
use App\Web\Pages\Settings\StorageProviders\Actions\Create;
|
||||
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 implements HasSecondSubNav
|
||||
{
|
||||
use Traits\Navigation;
|
||||
|
||||
protected static ?string $slug = 'servers/{server}/databases/backups';
|
||||
|
||||
protected static ?string $title = 'Backups';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [Backup::class, $this->server]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return [
|
||||
Action::make('create')
|
||||
->icon('heroicon-o-plus')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->authorize(fn () => $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) => ManageBackup::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) => ManageBackup::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 () => $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) => ManageBackup::rules($this->server, $get())['interval']),
|
||||
TextInput::make('custom_interval')
|
||||
->label('Custom Interval (Cron)')
|
||||
->rules(fn (callable $get) => ManageBackup::rules($this->server, $get())['custom_interval'])
|
||||
->visible(fn (callable $get): bool => $get('interval') === 'custom')
|
||||
->placeholder('0 * * * *'),
|
||||
TextInput::make('keep')
|
||||
->label('Backups to Keep')
|
||||
->rules(fn (callable $get) => ManageBackup::rules($this->server, $get())['keep'])
|
||||
->helperText('How many backups to keep before deleting the oldest one'),
|
||||
])
|
||||
->modalSubmitActionLabel('Create')
|
||||
->action(function (array $data): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
app(ManageBackup::class)->create($this->server, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Backup created!')
|
||||
->send();
|
||||
});
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\BackupsList::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
}
|
@ -1,163 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Databases;
|
||||
|
||||
use App\Actions\Database\CreateDatabase;
|
||||
use App\Actions\Database\SyncDatabases;
|
||||
use App\Models\Database;
|
||||
use App\Models\Server;
|
||||
use App\Web\Contracts\HasSecondSubNav;
|
||||
use App\Web\Pages\Servers\Page;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page implements HasSecondSubNav
|
||||
{
|
||||
use Traits\Navigation;
|
||||
|
||||
protected static ?string $slug = 'servers/{server}/databases';
|
||||
|
||||
protected static ?string $title = 'Databases';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [Database::class, $this->server]);
|
||||
}
|
||||
|
||||
public static function getCharsetInput(Server $server): Select
|
||||
{
|
||||
return Select::make('charset')
|
||||
->label('Charset / Encoding')
|
||||
->native(false)
|
||||
->live()
|
||||
->default(function () use ($server) {
|
||||
$service = $server->defaultService('database');
|
||||
|
||||
return $service->type_data['defaultCharset'] ?? null;
|
||||
})
|
||||
->options(function () use ($server): array {
|
||||
$service = $server->defaultService('database');
|
||||
$charsets = $service->type_data['charsets'] ?? [];
|
||||
|
||||
return array_combine(
|
||||
array_keys($charsets),
|
||||
array_keys($charsets)
|
||||
);
|
||||
})
|
||||
->afterStateUpdated(function (Get $get, Set $set, $state) use ($server): void {
|
||||
$service = $server->defaultService('database');
|
||||
$charsets = $service->type_data['charsets'] ?? [];
|
||||
$set('collation', $charsets[$state]['default'] ?? null);
|
||||
})
|
||||
->rules(fn (callable $get) => CreateDatabase::rules($server, $get())['charset']);
|
||||
}
|
||||
|
||||
public static function getCollationInput(Server $server): Select
|
||||
{
|
||||
return Select::make('collation')
|
||||
->label('Collation')
|
||||
->native(false)
|
||||
->live()
|
||||
->default(function (Get $get) use ($server) {
|
||||
$service = $server->defaultService('database');
|
||||
$charsets = $service->type_data['charsets'] ?? [];
|
||||
$charset = $get('charset') ?? $service->type_data['default'] ?? 'utf8mb4';
|
||||
|
||||
return $charsets[$charset]['default'] ?? null;
|
||||
})
|
||||
->options(function (Get $get) use ($server): array {
|
||||
$service = $server->defaultService('database');
|
||||
$collations = $service->type_data['charsets'][$get('charset')]['list'] ?? [];
|
||||
|
||||
return array_combine(
|
||||
array_values($collations),
|
||||
array_values($collations)
|
||||
);
|
||||
})
|
||||
->rules(fn (callable $get) => CreateDatabase::rules($server, $get())['collation']);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('sync')
|
||||
->color('gray')
|
||||
->label('Sync Databases')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->authorize(fn () => auth()->user()?->can('create', [Database::class, $this->server]))
|
||||
->requiresConfirmation()
|
||||
->modalDescription('This will create databases that exist on the server but not in Vito.')
|
||||
->modalSubmitActionLabel('Sync')
|
||||
->action(function (): void {
|
||||
run_action($this, function (): void {
|
||||
app(SyncDatabases::class)->sync($this->server);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Databases synced!')
|
||||
->send();
|
||||
});
|
||||
}),
|
||||
Action::make('create')
|
||||
->label('Create Database')
|
||||
->icon('heroicon-o-plus')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->authorize(fn () => auth()->user()?->can('create', [Database::class, $this->server]))
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->rules(fn (callable $get) => CreateDatabase::rules($this->server, $get())['name']),
|
||||
self::getCharsetInput($this->server),
|
||||
self::getCollationInput($this->server),
|
||||
Checkbox::make('user')
|
||||
->label('Create User')
|
||||
->default(false)
|
||||
->reactive(),
|
||||
TextInput::make('username')
|
||||
->label('Username')
|
||||
->rules(fn (callable $get) => CreateDatabase::rules($this->server, $get())['username'])
|
||||
->hidden(fn (callable $get): bool => $get('user') !== true),
|
||||
TextInput::make('password')
|
||||
->label('Password')
|
||||
->rules(fn (callable $get) => CreateDatabase::rules($this->server, $get())['password'])
|
||||
->hidden(fn (callable $get): bool => $get('user') !== true),
|
||||
Checkbox::make('remote')
|
||||
->label('Remote')
|
||||
->default(false)
|
||||
->hidden(fn (callable $get): bool => $get('user') !== true)
|
||||
->reactive(),
|
||||
TextInput::make('host')
|
||||
->label('Host')
|
||||
->rules(fn (callable $get) => CreateDatabase::rules($this->server, $get())['host'])
|
||||
->hidden(fn (callable $get): bool => $get('remote') !== true),
|
||||
])
|
||||
->modalSubmitActionLabel('Create')
|
||||
->action(function (array $data): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
app(CreateDatabase::class)->create($this->server, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Database Created!')
|
||||
->send();
|
||||
});
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\DatabasesList::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Databases\Traits;
|
||||
|
||||
use App\Models\Backup;
|
||||
use App\Models\Database;
|
||||
use App\Models\DatabaseUser;
|
||||
use App\Models\User;
|
||||
use App\Web\Pages\Servers\Databases\Backups;
|
||||
use App\Web\Pages\Servers\Databases\Index as Databases;
|
||||
use App\Web\Pages\Servers\Databases\Users;
|
||||
use Filament\Navigation\NavigationGroup;
|
||||
use Filament\Navigation\NavigationItem;
|
||||
|
||||
trait Navigation
|
||||
{
|
||||
public function getSecondSubNavigation(): array
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
$items = [];
|
||||
|
||||
if ($user->can('viewAny', [Database::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(Databases::getNavigationLabel())
|
||||
->icon('heroicon-o-circle-stack')
|
||||
->isActiveWhen(fn () => request()->routeIs(Databases::getRouteName()))
|
||||
->url(Databases::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('viewAny', [DatabaseUser::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(Users::getNavigationLabel())
|
||||
->icon('heroicon-o-users')
|
||||
->isActiveWhen(fn () => request()->routeIs(Users::getRouteName()))
|
||||
->url(Users::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('viewAny', [Backup::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(Backups::getNavigationLabel())
|
||||
->icon('heroicon-o-cloud')
|
||||
->isActiveWhen(fn () => request()->routeIs(Backups::getRouteName()))
|
||||
->url(Backups::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
return [
|
||||
NavigationGroup::make()
|
||||
->items($items),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Databases;
|
||||
|
||||
use App\Actions\Database\CreateDatabase;
|
||||
use App\Actions\Database\CreateDatabaseUser;
|
||||
use App\Actions\Database\SyncDatabaseUsers;
|
||||
use App\Models\DatabaseUser;
|
||||
use App\Web\Contracts\HasSecondSubNav;
|
||||
use App\Web\Pages\Servers\Page;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Users extends Page implements HasSecondSubNav
|
||||
{
|
||||
use Traits\Navigation;
|
||||
|
||||
protected static ?string $slug = 'servers/{server}/databases/users';
|
||||
|
||||
protected static ?string $title = 'Database Users';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [DatabaseUser::class, $this->server]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('sync')
|
||||
->color('gray')
|
||||
->label('Sync Users')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->authorize(fn () => auth()->user()?->can('create', [DatabaseUser::class, $this->server]))
|
||||
->requiresConfirmation()
|
||||
->modalDescription('This will create db users that exist on the server but not in Vito.')
|
||||
->modalSubmitActionLabel('Sync')
|
||||
->action(function (): void {
|
||||
run_action($this, function (): void {
|
||||
app(SyncDatabaseUsers::class)->sync($this->server);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Users synced!')
|
||||
->send();
|
||||
});
|
||||
}),
|
||||
Action::make('create')
|
||||
->icon('heroicon-o-plus')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->authorize(fn () => auth()->user()?->can('create', [DatabaseUser::class, $this->server]))
|
||||
->form([
|
||||
TextInput::make('username')
|
||||
->label('Username')
|
||||
->rules(fn (callable $get) => CreateDatabaseUser::rules($this->server, $get())['username']),
|
||||
TextInput::make('password')
|
||||
->label('Password')
|
||||
->rules(fn (callable $get) => CreateDatabaseUser::rules($this->server, $get())['password']),
|
||||
Checkbox::make('remote')
|
||||
->label('Remote')
|
||||
->default(false)
|
||||
->visible(in_array($this->server->database()->name, ['mysql', 'mariadb']))
|
||||
->reactive(),
|
||||
TextInput::make('host')
|
||||
->label('Host')
|
||||
->rules(fn (callable $get) => CreateDatabase::rules($this->server, $get())['host'])
|
||||
->hidden(fn (callable $get): bool => $get('remote') !== true),
|
||||
])
|
||||
->modalSubmitActionLabel('Create')
|
||||
->action(function (array $data): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
app(CreateDatabaseUser::class)->create($this->server, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Database user created!')
|
||||
->send();
|
||||
});
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\DatabaseUsersList::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
}
|
@ -1,136 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Databases\Widgets;
|
||||
|
||||
use App\Actions\Database\ManageBackupFile;
|
||||
use App\Actions\Database\RestoreBackup;
|
||||
use App\Models\Backup;
|
||||
use App\Models\BackupFile;
|
||||
use App\Models\Database;
|
||||
use App\Models\User;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<BackupFile>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return BackupFile::query()->where('backup_id', $this->backup->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
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): string => $record->getDateTimeByTimezone($record->restored_at))
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->badge()
|
||||
->color(fn ($state) => BackupFile::$statusColors[$state])
|
||||
->sortable(),
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<BackupFile> $query
|
||||
* @return Builder<BackupFile>
|
||||
*/
|
||||
protected function applyDefaultSortingToTableQuery(Builder $query): Builder
|
||||
{
|
||||
return $query->latest('created_at');
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $table
|
||||
->heading(null)
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->actions([
|
||||
Action::make('download')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->visible(fn (BackupFile $record): bool => $record->isAvailable() && $record->isLocal())
|
||||
->tooltip('Download')
|
||||
->action(fn (BackupFile $record) => app(ManageBackupFile::class)->download($record))
|
||||
->authorize(fn (BackupFile $record) => $user->can('view', $record)),
|
||||
Action::make('restore')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->modalHeading('Restore Backup')
|
||||
->tooltip('Restore Backup')
|
||||
->disabled(fn (BackupFile $record): bool => ! $record->isAvailable())
|
||||
->authorize(fn (BackupFile $record) => $user->can('update', $record->backup))
|
||||
->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): void {
|
||||
run_action($this, function () use ($record, $data): void {
|
||||
$this->validate();
|
||||
|
||||
/** @var Database $database */
|
||||
$database = Database::query()->findOrFail($data['database']);
|
||||
|
||||
$this->authorize('update', $database);
|
||||
|
||||
app(RestoreBackup::class)->restore($record, $data);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Backup is being restored')
|
||||
->send();
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}),
|
||||
Action::make('delete')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-trash')
|
||||
->modalHeading('Delete Backup File')
|
||||
->color('danger')
|
||||
->disabled(fn (BackupFile $record): bool => ! $record->isAvailable())
|
||||
->tooltip('Delete')
|
||||
->authorize(fn (BackupFile $record) => $user->can('delete', $record))
|
||||
->requiresConfirmation()
|
||||
->action(function (BackupFile $record): void {
|
||||
app(ManageBackupFile::class)->delete($record);
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,160 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Databases\Widgets;
|
||||
|
||||
use App\Actions\Database\ManageBackup;
|
||||
use App\Actions\Database\RunBackup;
|
||||
use App\Models\Backup;
|
||||
use App\Models\BackupFile;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<Backup>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Backup::query()->where('server_id', $this->server->id);
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('database.name')
|
||||
->label('Database')
|
||||
->tooltip(fn (Backup $record) => $record->database->deleted_at ? 'Deleted at '.$record->database->deleted_at->format('Y-m-d H:i:s') : null)
|
||||
->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 table(Table $table): Table
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $table
|
||||
->heading(null)
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->actions([
|
||||
Action::make('edit')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-pencil')
|
||||
->tooltip('Edit Configuration')
|
||||
->disabled(fn (Backup $record): bool => ! in_array($record->status, ['running', 'failed']))
|
||||
->authorize(fn (Backup $record) => $user->can('update', $record))
|
||||
->modelLabel('Edit Backup')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->modalSubmitActionLabel('Update')
|
||||
->form([
|
||||
Select::make('interval')
|
||||
->label('Interval')
|
||||
->options(config('core.cronjob_intervals'))
|
||||
->reactive()
|
||||
->default(fn (Backup $record) => $record->isCustomInterval() ? 'custom' : $record->interval)
|
||||
->rules(fn (callable $get) => ManageBackup::rules($this->server, $get())['interval']),
|
||||
TextInput::make('custom_interval')
|
||||
->label('Custom Interval (Cron)')
|
||||
->rules(fn (callable $get) => ManageBackup::rules($this->server, $get())['custom_interval'])
|
||||
->visible(fn (callable $get): bool => $get('interval') === 'custom')
|
||||
->default(fn (Backup $record) => $record->isCustomInterval() ? $record->interval : '')
|
||||
->placeholder('0 * * * *'),
|
||||
TextInput::make('keep')
|
||||
->label('Backups to Keep')
|
||||
->default(fn (Backup $record) => $record->keep_backups)
|
||||
->rules(fn (callable $get) => ManageBackup::rules($this->server, $get())['keep'])
|
||||
->helperText('How many backups to keep before deleting the oldest one'),
|
||||
])
|
||||
->action(function (Backup $backup, array $data): void {
|
||||
run_action($this, function () use ($data, $backup): void {
|
||||
app(ManageBackup::class)->update($backup, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Backup updated!')
|
||||
->send();
|
||||
});
|
||||
}),
|
||||
Action::make('files')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-rectangle-stack')
|
||||
->modalHeading('Backup Files')
|
||||
->color('gray')
|
||||
->tooltip('Show backup files')
|
||||
->disabled(fn (Backup $record): bool => ! in_array($record->status, ['running', 'failed']))
|
||||
->authorize(fn (Backup $record) => $user->can('viewAny', [BackupFile::class, $record]))
|
||||
->modalContent(fn (Backup $record) => view('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): void {
|
||||
app(RunBackup::class)->run($record);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
]),
|
||||
Action::make('delete')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-trash')
|
||||
->modalHeading('Delete Backup & Files')
|
||||
->disabled(fn (Backup $record): bool => ! in_array($record->status, ['running', 'failed']))
|
||||
->color('danger')
|
||||
->tooltip('Delete')
|
||||
->authorize(fn (Backup $record) => $user->can('delete', $record))
|
||||
->requiresConfirmation()
|
||||
->action(function (Backup $record): void {
|
||||
app(ManageBackup::class)->delete($record);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Databases\Widgets;
|
||||
|
||||
use App\Actions\Database\DeleteDatabaseUser;
|
||||
use App\Actions\Database\LinkUser;
|
||||
use App\Models\DatabaseUser;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
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 DatabaseUsersList extends Widget
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<DatabaseUser>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return DatabaseUser::query()->where('server_id', $this->server->id);
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('username')
|
||||
->searchable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (DatabaseUser $databaseUser) => DatabaseUser::$statusColors[$databaseUser->status])
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('Created At')
|
||||
->formatStateUsing(fn ($record) => $record->created_at_by_timezone)
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->heading(null)
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->actions([
|
||||
$this->passwordAction(),
|
||||
$this->linkAction(),
|
||||
$this->deleteAction(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function passwordAction(): Action
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return Action::make('password')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-key')
|
||||
->color('gray')
|
||||
->modalHeading('Database user\'s password')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->tooltip('Show the password')
|
||||
->authorize(fn ($record) => $user->can('view', $record))
|
||||
->form([
|
||||
TextInput::make('password')
|
||||
->label('Password')
|
||||
->default(fn (DatabaseUser $record) => $record->password)
|
||||
->disabled(),
|
||||
])
|
||||
->action(function (DatabaseUser $record, array $data): void {
|
||||
//
|
||||
})
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelActionLabel('Close');
|
||||
}
|
||||
|
||||
private function linkAction(): Action
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return Action::make('link')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-link')
|
||||
->modalHeading('Link user to databases')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->tooltip('Link user')
|
||||
->modalSubmitActionLabel('Save')
|
||||
->authorize(fn ($record) => $user->can('update', $record))
|
||||
->form([
|
||||
CheckboxList::make('databases')
|
||||
->label('Databases')
|
||||
->options($this->server->databases()->pluck('name', 'name')->toArray())
|
||||
->rules(fn (callable $get): array => LinkUser::rules($this->server, $get()))
|
||||
->default(fn (DatabaseUser $record) => $record->databases),
|
||||
])
|
||||
->action(function (DatabaseUser $record, array $data): void {
|
||||
run_action($this, function () use ($record, $data): void {
|
||||
app(LinkUser::class)->link($record, $data);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('User linked to databases!')
|
||||
->send();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private function deleteAction(): Action
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return Action::make('delete')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-trash')
|
||||
->modalHeading('Delete Database User')
|
||||
->color('danger')
|
||||
->tooltip('Delete')
|
||||
->authorize(fn ($record) => $user->can('delete', $record))
|
||||
->requiresConfirmation()
|
||||
->action(function (DatabaseUser $record): void {
|
||||
run_action($this, function () use ($record): void {
|
||||
app(DeleteDatabaseUser::class)->delete($this->server, $record);
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Databases\Widgets;
|
||||
|
||||
use App\Actions\Database\DeleteDatabase;
|
||||
use App\Models\Database;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
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 DatabasesList extends Widget
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<Database>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Database::query()->where('server_id', $this->server->id);
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('charset')
|
||||
->label('Charset / Encoding')
|
||||
->sortable(),
|
||||
TextColumn::make('collation')
|
||||
->label('Collation')
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (Database $database) => Database::$statusColors[$database->status])
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('Created At')
|
||||
->formatStateUsing(fn ($record) => $record->created_at_by_timezone)
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $table
|
||||
->heading(null)
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->actions([
|
||||
Action::make('delete')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-trash')
|
||||
->modalHeading('Delete Database')
|
||||
->color('danger')
|
||||
->tooltip('Delete')
|
||||
->authorize(fn ($record) => $user->can('delete', $record))
|
||||
->requiresConfirmation()
|
||||
->action(function (Database $record): void {
|
||||
run_action($this, function () use ($record): void {
|
||||
app(DeleteDatabase::class)->delete($this->server, $record);
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\FileManager;
|
||||
|
||||
use App\Web\Pages\Servers\Page;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/file-manager';
|
||||
|
||||
protected static ?string $title = 'File Manager';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('manage', $this->server);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\FilesList::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
}
|
@ -1,379 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\FileManager\Widgets;
|
||||
|
||||
use App\Actions\FileManager\FetchFiles;
|
||||
use App\Exceptions\SSHError;
|
||||
use App\Models\File;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Web\Fields\CodeEditorField;
|
||||
use App\Web\Pages\Servers\FileManager\Index;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Support\Enums\ActionSize;
|
||||
use Filament\Support\Enums\IconPosition;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class FilesList extends Widget
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public string $serverUser;
|
||||
|
||||
public string $path;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->serverUser = $this->server->ssh_user;
|
||||
$this->path = home_path($this->serverUser);
|
||||
if (request()->has('path') && request()->has('user')) {
|
||||
$this->path = request('path');
|
||||
$this->serverUser = request('user');
|
||||
}
|
||||
$this->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
protected function getTableHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->homeAction(),
|
||||
$this->userAction(),
|
||||
ActionGroup::make([
|
||||
$this->refreshAction(),
|
||||
$this->newFileAction(),
|
||||
$this->newDirectoryAction(),
|
||||
$this->uploadAction(),
|
||||
])
|
||||
->tooltip('Toolbar')
|
||||
->icon('heroicon-o-ellipsis-vertical')
|
||||
->color('gray')
|
||||
->size(ActionSize::Large)
|
||||
->iconPosition(IconPosition::After)
|
||||
->dropdownPlacement('bottom-end'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Builder<File>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return File::query()
|
||||
->where('user_id', auth()->id())
|
||||
->where('server_id', $this->server->id);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
auth()->user();
|
||||
|
||||
return $table
|
||||
->query($this->getTableQuery())
|
||||
->headerActions($this->getTableHeaderActions())
|
||||
->heading(str($this->path)->substr(-50)->start(str($this->path)->length() > 50 ? '...' : ''))
|
||||
->columns([
|
||||
IconColumn::make('type')
|
||||
->sortable()
|
||||
->icon(fn (File $file): string => $this->getIcon($file)),
|
||||
TextColumn::make('name')
|
||||
->sortable(),
|
||||
TextColumn::make('size')
|
||||
->sortable(),
|
||||
TextColumn::make('owner')
|
||||
->sortable(),
|
||||
TextColumn::make('group')
|
||||
->sortable(),
|
||||
TextColumn::make('date')
|
||||
->sortable(),
|
||||
TextColumn::make('permissions')
|
||||
->sortable(),
|
||||
])
|
||||
->recordUrl(function (File $file): string {
|
||||
if ($file->type === 'directory') {
|
||||
return Index::getUrl([
|
||||
'server' => $this->server->id,
|
||||
'user' => $file->server_user,
|
||||
'path' => absolute_path($file->path.'/'.$file->name),
|
||||
]);
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->defaultSort('type')
|
||||
->actions([
|
||||
$this->extractAction(),
|
||||
$this->downloadAction(),
|
||||
$this->editAction(),
|
||||
$this->deleteAction(),
|
||||
])
|
||||
->checkIfRecordIsSelectableUsing(
|
||||
fn (File $file): bool => $file->name !== '..',
|
||||
)
|
||||
->bulkActions([
|
||||
DeleteBulkAction::make()
|
||||
->requiresConfirmation(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function changeUser(string $user): void
|
||||
{
|
||||
$this->redirect(
|
||||
Index::getUrl([
|
||||
'server' => $this->server->id,
|
||||
'user' => $user,
|
||||
'path' => home_path($user),
|
||||
]),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
try {
|
||||
app(FetchFiles::class)->fetch(
|
||||
$user,
|
||||
$this->server,
|
||||
[
|
||||
'user' => $this->serverUser,
|
||||
'path' => $this->path,
|
||||
]
|
||||
);
|
||||
} catch (SSHError) {
|
||||
abort(404);
|
||||
}
|
||||
$this->dispatch('$refresh');
|
||||
}
|
||||
|
||||
protected function getIcon(File $file): string
|
||||
{
|
||||
if ($file->type === 'directory') {
|
||||
return 'heroicon-o-folder';
|
||||
}
|
||||
|
||||
if (str($file->name)->endsWith('.blade.php')) {
|
||||
return 'laravel';
|
||||
}
|
||||
|
||||
if (str($file->name)->endsWith('.php')) {
|
||||
return 'php';
|
||||
}
|
||||
|
||||
return 'heroicon-o-document-text';
|
||||
}
|
||||
|
||||
protected function homeAction(): Action
|
||||
{
|
||||
return Action::make('home')
|
||||
->label('Home')
|
||||
->size(ActionSize::Small)
|
||||
->icon('heroicon-o-home')
|
||||
->action(function (): void {
|
||||
$this->path = home_path($this->serverUser);
|
||||
$this->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
protected function userAction(): ActionGroup
|
||||
{
|
||||
$users = [];
|
||||
foreach ($this->server->getSshUsers() as $user) {
|
||||
$users[] = Action::make('user-'.$user)
|
||||
->action(fn () => $this->changeUser($user))
|
||||
->label($user);
|
||||
}
|
||||
|
||||
return ActionGroup::make($users)
|
||||
->tooltip('Change user')
|
||||
->label($this->serverUser)
|
||||
->button()
|
||||
->size(ActionSize::Small)
|
||||
->color('gray')
|
||||
->icon('heroicon-o-chevron-up-down')
|
||||
->iconPosition(IconPosition::After)
|
||||
->dropdownPlacement('bottom-end');
|
||||
}
|
||||
|
||||
protected function refreshAction(): Action
|
||||
{
|
||||
return Action::make('refresh')
|
||||
->label('Refresh')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->action(fn () => $this->refresh());
|
||||
}
|
||||
|
||||
protected function newFileAction(): Action
|
||||
{
|
||||
return Action::make('new-file')
|
||||
->label('New File')
|
||||
->icon('heroicon-o-document-text')
|
||||
->action(function (array $data): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
$this->server->os()->write(
|
||||
$this->path.'/'.$data['name'],
|
||||
str_replace("\r\n", "\n", $data['content']),
|
||||
$this->serverUser
|
||||
);
|
||||
$this->refresh();
|
||||
});
|
||||
})
|
||||
->form(fn (): array => [
|
||||
TextInput::make('name')
|
||||
->placeholder('file-name.txt'),
|
||||
CodeEditorField::make('content'),
|
||||
])
|
||||
->modalSubmitActionLabel('Create')
|
||||
->modalHeading('New File')
|
||||
->modalWidth('4xl');
|
||||
}
|
||||
|
||||
protected function newDirectoryAction(): Action
|
||||
{
|
||||
return Action::make('new-directory')
|
||||
->label('New Directory')
|
||||
->icon('heroicon-o-folder')
|
||||
->action(function (array $data): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
$this->server->os()->mkdir(
|
||||
$this->path.'/'.$data['name'],
|
||||
$this->serverUser
|
||||
);
|
||||
$this->refresh();
|
||||
});
|
||||
})
|
||||
->form(fn (): array => [
|
||||
TextInput::make('name')
|
||||
->placeholder('directory name'),
|
||||
])
|
||||
->modalSubmitActionLabel('Create')
|
||||
->modalHeading('New Directory')
|
||||
->modalWidth('lg');
|
||||
}
|
||||
|
||||
protected function uploadAction(): Action
|
||||
{
|
||||
return Action::make('upload')
|
||||
->label('Upload File')
|
||||
->icon('heroicon-o-arrow-up-on-square')
|
||||
->action(function (array $data): void {
|
||||
//
|
||||
})
|
||||
->after(function (array $data): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
foreach ($data['file'] as $file) {
|
||||
$this->server->ssh()->upload(
|
||||
Storage::disk('tmp')->path($file),
|
||||
$this->path.'/'.$file,
|
||||
$this->serverUser
|
||||
);
|
||||
}
|
||||
$this->refresh();
|
||||
});
|
||||
})
|
||||
->form(fn (): array => [
|
||||
FileUpload::make('file')
|
||||
->disk('tmp')
|
||||
->multiple()
|
||||
->preserveFilenames(),
|
||||
])
|
||||
->modalSubmitActionLabel('Upload to Server')
|
||||
->modalHeading('Upload File')
|
||||
->modalWidth('xl');
|
||||
}
|
||||
|
||||
protected function extractAction(): Action
|
||||
{
|
||||
return Action::make('extract')
|
||||
->tooltip('Extract')
|
||||
->icon('heroicon-o-archive-box')
|
||||
->hiddenLabel()
|
||||
->visible(fn (File $file): bool => $file->isExtractable())
|
||||
->action(function (File $file): void {
|
||||
$file->server->os()->extract($file->getFilePath(), $file->path, $file->server_user);
|
||||
$this->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
protected function downloadAction(): Action
|
||||
{
|
||||
return Action::make('download')
|
||||
->tooltip('Download')
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->hiddenLabel()
|
||||
->visible(fn (File $file): bool => $file->type === 'file')
|
||||
->action(function (File $file) {
|
||||
$file->server->ssh($file->server_user)->download(
|
||||
Storage::disk('tmp')->path($file->name),
|
||||
$file->getFilePath()
|
||||
);
|
||||
|
||||
return Storage::disk('tmp')->download($file->name);
|
||||
});
|
||||
}
|
||||
|
||||
protected function editAction(): Action
|
||||
{
|
||||
return Action::make('edit')
|
||||
->tooltip('Edit')
|
||||
->icon('heroicon-o-pencil')
|
||||
->hiddenLabel()
|
||||
->visible(fn (File $file): bool => $file->type === 'file')
|
||||
->action(function (File $file, array $data): void {
|
||||
$file->server->os()->write(
|
||||
$file->getFilePath(),
|
||||
str_replace("\r\n", "\n", $data['content']),
|
||||
$file->server_user
|
||||
);
|
||||
$this->refresh();
|
||||
})
|
||||
->form(fn (File $file): array => [
|
||||
CodeEditorField::make('content')
|
||||
->formatStateUsing(function () use ($file) {
|
||||
$file->server->ssh($file->server_user)->download(
|
||||
Storage::disk('tmp')->path($file->name),
|
||||
$file->getFilePath()
|
||||
);
|
||||
|
||||
return Storage::disk('tmp')->get(basename($file->getFilePath()));
|
||||
}),
|
||||
])
|
||||
->modalSubmitActionLabel('Save')
|
||||
->modalHeading('Edit')
|
||||
->modalWidth('4xl');
|
||||
}
|
||||
|
||||
protected function deleteAction(): Action
|
||||
{
|
||||
return Action::make('delete')
|
||||
->tooltip('Delete')
|
||||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->hiddenLabel()
|
||||
->requiresConfirmation()
|
||||
->visible(fn (File $file): bool => $file->name !== '..')
|
||||
->action(function (File $file): void {
|
||||
run_action($this, function () use ($file): void {
|
||||
$file->delete();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Firewall;
|
||||
|
||||
use App\Actions\FirewallRule\ManageRule;
|
||||
use App\Models\FirewallRule;
|
||||
use App\Web\Pages\Servers\Page;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/firewall';
|
||||
|
||||
protected static ?string $title = 'Firewall';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [FirewallRule::class, $this->server]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\RulesList::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
public static function getFirewallForm(?FirewallRule $record = null): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('name')
|
||||
->label('Purpose')
|
||||
->default($record->name ?? null)
|
||||
->rules(ManageRule::rules()['name']),
|
||||
Select::make('type')
|
||||
->label('Type')
|
||||
->default($record->type ?? 'allow')
|
||||
->options([
|
||||
'allow' => 'Allow',
|
||||
'deny' => 'Deny',
|
||||
])
|
||||
->rules(ManageRule::rules()['type']),
|
||||
Select::make('protocol')
|
||||
->label('Protocol')
|
||||
->default($record->protocol ?? 'tcp')
|
||||
->options([
|
||||
'tcp' => 'TCP',
|
||||
'udp' => 'UDP',
|
||||
])
|
||||
->rules(ManageRule::rules()['protocol']),
|
||||
TextInput::make('port')
|
||||
->label('Port')
|
||||
->default($record->port ?? null)
|
||||
->rules(['required', 'integer']),
|
||||
Checkbox::make('source_any')
|
||||
->label('Any Source')
|
||||
->default(($record->source ?? null) == null)
|
||||
->rules(['boolean'])
|
||||
->helperText('Allow connections from any source, regardless of their IP address or subnet mask.')
|
||||
->live(),
|
||||
TextInput::make('source')
|
||||
->hidden(fn (Get $get): bool => $get('source_any') == true)
|
||||
->label('Source')
|
||||
->helperText('The IP address of the source of the connection.')
|
||||
->rules(ManageRule::rules()['source'])
|
||||
->default($record->source ?? null)
|
||||
->suffixAction(
|
||||
\Filament\Forms\Components\Actions\Action::make('get_ip')
|
||||
->icon('heroicon-o-globe-alt')
|
||||
->color('primary')
|
||||
->tooltip('Use My IP')
|
||||
->action(function ($set): void {
|
||||
$ip = Request::ip();
|
||||
$set('source', $ip);
|
||||
})
|
||||
),
|
||||
TextInput::make('mask')
|
||||
->hidden(fn (Get $get): bool => $get('source_any') == true)
|
||||
->label('Mask')
|
||||
->default($record->mask ?? null)
|
||||
->helperText('The subnet mask of the source of the connection. Leave blank for a single IP address.')
|
||||
->rules(ManageRule::rules()['mask']),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('read-the-docs')
|
||||
->label('Read the Docs')
|
||||
->icon('heroicon-o-document-text')
|
||||
->color('gray')
|
||||
->url('https://vitodeploy.com/servers/firewall')
|
||||
->openUrlInNewTab(),
|
||||
Action::make('create')
|
||||
->authorize(fn () => auth()->user()?->can('create', [FirewallRule::class, $this->server]))
|
||||
->label('Create a Rule')
|
||||
->icon('heroicon-o-plus')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->modalHeading('Create Firewall Rule')
|
||||
->modalDescription('Add a new rule to the firewall')
|
||||
->modalSubmitActionLabel('Create')
|
||||
->form(self::getFirewallForm())
|
||||
->action(function (array $data): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
app(ManageRule::class)->create($this->server, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Applying Firewall Rule')
|
||||
->send();
|
||||
});
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Firewall\Widgets;
|
||||
|
||||
use App\Actions\FirewallRule\ManageRule;
|
||||
use App\Models\FirewallRule;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Web\Pages\Servers\Firewall\Index;
|
||||
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;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RulesList extends Widget
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<FirewallRule>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return FirewallRule::query()->where('server_id', $this->server->id);
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->label('Purpose'),
|
||||
TextColumn::make('type')
|
||||
->sortable()
|
||||
->badge()
|
||||
->color(fn ($state): string => $state === 'allow' ? 'success' : 'warning')
|
||||
->label('Type')
|
||||
->formatStateUsing(fn ($state) => Str::upper($state)),
|
||||
TextColumn::make('id')
|
||||
->sortable()
|
||||
->label('Source')
|
||||
->formatStateUsing(function (FirewallRule $record) {
|
||||
$source = $record->source == null ? 'any' : $record->source;
|
||||
if ($source !== 'any' && $record->mask !== null) {
|
||||
$source .= '/'.$record->mask;
|
||||
}
|
||||
|
||||
return $source;
|
||||
}),
|
||||
TextColumn::make('protocol')
|
||||
->sortable()
|
||||
->badge()
|
||||
->color('primary')
|
||||
->label('Protocol')
|
||||
->formatStateUsing(fn ($state) => Str::upper($state)),
|
||||
TextColumn::make('port')
|
||||
->sortable()
|
||||
->label('Port'),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (FirewallRule $record): string => $record->getStatusColor()),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $table
|
||||
->heading(null)
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->actions([
|
||||
Action::make('edit')
|
||||
->icon('heroicon-o-pencil')
|
||||
->tooltip('Edit')
|
||||
->hiddenLabel()
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->modalHeading('Edit Firewall Rule')
|
||||
->modalDescription('Edit the associated servers firewall rule.')
|
||||
->modalSubmitActionLabel('Update')
|
||||
->authorize(fn (FirewallRule $record) => $user->can('update', $record))
|
||||
->form(fn ($record): array => Index::getFirewallForm($record))
|
||||
->action(function (FirewallRule $record, array $data): void {
|
||||
run_action($this, function () use ($record, $data): void {
|
||||
app(ManageRule::class)->update($record, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Applying Firewall Rule')
|
||||
->send();
|
||||
});
|
||||
}),
|
||||
Action::make('delete')
|
||||
->icon('heroicon-o-trash')
|
||||
->tooltip('Delete')
|
||||
->color('danger')
|
||||
->hiddenLabel()
|
||||
->requiresConfirmation()
|
||||
->authorize(fn (FirewallRule $record) => $user->can('delete', $record))
|
||||
->action(function (FirewallRule $record): void {
|
||||
try {
|
||||
app(ManageRule::class)->delete($record);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,251 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers;
|
||||
|
||||
use App\Actions\Server\CreateServer as CreateServerAction;
|
||||
use App\Enums\Database;
|
||||
use App\Enums\PHP;
|
||||
use App\Enums\ServerProvider;
|
||||
use App\Enums\Webserver;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Web\Components\Page;
|
||||
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\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-server-stack';
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
protected static ?string $title = 'Servers';
|
||||
|
||||
public static function getNavigationItemActiveRoutePattern(): string
|
||||
{
|
||||
return static::getRouteName().'*';
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
$this->authorize('viewAny', [Server::class, $user->currentProject]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\ServersList::class],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
$publicKey = __('servers.create.public_key_text', [
|
||||
'public_key' => get_public_key_content(),
|
||||
]);
|
||||
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
/** @var Project $project */
|
||||
$project = $user->currentProject;
|
||||
|
||||
return [
|
||||
\Filament\Actions\Action::make('read-the-docs')
|
||||
->label('Read the Docs')
|
||||
->icon('heroicon-o-document-text')
|
||||
->color('gray')
|
||||
->url('https://vitodeploy.com')
|
||||
->openUrlInNewTab(),
|
||||
\Filament\Actions\Action::make('create')
|
||||
->label('Create a Server')
|
||||
->icon('heroicon-o-plus')
|
||||
->authorize('create', [Server::class, $user->currentProject])
|
||||
->modalWidth(MaxWidth::FiveExtraLarge)
|
||||
->slideOver()
|
||||
->form([
|
||||
ProviderField::make('provider')
|
||||
->label('Select a provider')
|
||||
->default(ServerProvider::CUSTOM)
|
||||
->live()
|
||||
->reactive()
|
||||
->afterStateUpdated(function (callable $set): void {
|
||||
$set('server_provider', null);
|
||||
$set('region', null);
|
||||
$set('plan', null);
|
||||
})
|
||||
->rules(fn ($get) => CreateServerAction::rules($project, $get())['provider']),
|
||||
AlertField::make('alert')
|
||||
->warning()
|
||||
->message(__('servers.create.public_key_warning'))
|
||||
->visible(fn ($get): bool => $get('provider') === ServerProvider::CUSTOM),
|
||||
Select::make('server_provider')
|
||||
->visible(fn ($get): bool => $get('provider') !== ServerProvider::CUSTOM)
|
||||
->label('Server provider connection')
|
||||
->rules(fn ($get) => CreateServerAction::rules($project, $get())['server_provider'])
|
||||
->options(fn ($get) => \App\Models\ServerProvider::getByProjectId($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 () => $user->can('create', \App\Models\ServerProvider::class))
|
||||
->action(fn (array $data) => Create::action($data))
|
||||
)
|
||||
->placeholder('Select profile')
|
||||
->native(false)
|
||||
->live()
|
||||
->reactive()
|
||||
->selectablePlaceholder(false)
|
||||
->visible(fn ($get): bool => $get('provider') !== ServerProvider::CUSTOM),
|
||||
Grid::make()
|
||||
->schema([
|
||||
Select::make('region')
|
||||
->label('Region')
|
||||
->rules(fn ($get) => CreateServerAction::rules($project, $get())['region'] ?? [])
|
||||
->live()
|
||||
->reactive()
|
||||
->options(function ($get): array {
|
||||
if (! $get('server_provider')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return \App\Models\ServerProvider::regions($get('server_provider'));
|
||||
})
|
||||
->loadingMessage('Loading regions...')
|
||||
->disabled(fn ($get): bool => ! $get('server_provider'))
|
||||
->placeholder(fn ($get): string => $get('server_provider') ? 'Select region' : 'Select connection first')
|
||||
->searchable(),
|
||||
Select::make('plan')
|
||||
->label('Plan')
|
||||
->rules(fn ($get) => CreateServerAction::rules($project, $get())['plan'] ?? [])
|
||||
->reactive()
|
||||
->options(function ($get): array {
|
||||
if (! $get('server_provider') || ! $get('region')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return \App\Models\ServerProvider::plans($get('server_provider'), $get('region'));
|
||||
})
|
||||
->loadingMessage('Loading plans...')
|
||||
->disabled(fn ($get): bool => ! $get('region'))
|
||||
->placeholder(fn ($get): string => $get('region') ? 'Select plan' : 'Select plan first')
|
||||
->searchable(),
|
||||
])
|
||||
->visible(fn ($get): bool => $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, string $state): void {
|
||||
$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): bool => $get('provider') === ServerProvider::CUSTOM),
|
||||
Grid::make()
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->rules(fn ($get) => CreateServerAction::rules($project, $get())['name']),
|
||||
Select::make('os')
|
||||
->label('OS')
|
||||
->native(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($project, $get())['os'])
|
||||
->options(
|
||||
collect((array) config('core.operating_systems'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value])
|
||||
),
|
||||
]),
|
||||
Grid::make()
|
||||
->schema([
|
||||
TextInput::make('ip')
|
||||
->label('SSH IP Address')
|
||||
->rules(fn ($get) => CreateServerAction::rules($project, $get())['ip']),
|
||||
TextInput::make('port')
|
||||
->label('SSH Port')
|
||||
->rules(fn ($get) => CreateServerAction::rules($project, $get())['port']),
|
||||
])
|
||||
->visible(fn ($get): bool => $get('provider') === ServerProvider::CUSTOM),
|
||||
Fieldset::make('Services')
|
||||
->columns(1)
|
||||
->schema([
|
||||
AlertField::make('alert')
|
||||
->info()
|
||||
->message('You can install/uninstall services later'),
|
||||
Grid::make(3)
|
||||
->schema([
|
||||
Select::make('webserver')
|
||||
->label('Webserver')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($project, $get())['webserver'] ?? [])
|
||||
->default(Webserver::NONE)
|
||||
->options(
|
||||
collect((array) config('core.webservers'))->mapWithKeys(fn ($value) => [$value => $value])
|
||||
),
|
||||
Select::make('database')
|
||||
->label('Database')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($project, $get())['database'] ?? [])
|
||||
->default(Database::NONE)
|
||||
->options(
|
||||
collect((array) 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($project, $get())['php'] ?? [])
|
||||
->default(PHP::NONE)
|
||||
->options(
|
||||
collect((array) config('core.php_versions'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value])
|
||||
),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->modalSubmitActionLabel('Create')
|
||||
->action(function (array $data) use ($user, $project): void {
|
||||
run_action($this, function () use ($data, $user, $project): void {
|
||||
$server = app(CreateServerAction::class)->create($user, $project, $data);
|
||||
|
||||
$this->redirect(View::getUrl(['server' => $server]));
|
||||
});
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Logs;
|
||||
|
||||
use App\Models\ServerLog;
|
||||
use App\Web\Contracts\HasSecondSubNav;
|
||||
use App\Web\Pages\Servers\Logs\Widgets\LogsList;
|
||||
use App\Web\Pages\Servers\Page;
|
||||
|
||||
class Index extends Page implements HasSecondSubNav
|
||||
{
|
||||
use Traits\Navigation;
|
||||
|
||||
protected static ?string $slug = 'servers/{server}/logs';
|
||||
|
||||
protected static ?string $title = 'Logs';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [ServerLog::class, $this->server]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[LogsList::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Logs;
|
||||
|
||||
use App\Actions\Server\CreateServerLog;
|
||||
use App\Models\ServerLog;
|
||||
use App\Web\Contracts\HasSecondSubNav;
|
||||
use App\Web\Pages\Servers\Logs\Widgets\LogsList;
|
||||
use App\Web\Pages\Servers\Page;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class RemoteLogs extends Page implements HasSecondSubNav
|
||||
{
|
||||
use Traits\Navigation;
|
||||
|
||||
protected static ?string $slug = 'servers/{server}/logs/remote';
|
||||
|
||||
protected static ?string $title = 'Remote Logs';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [ServerLog::class, $this->server]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[LogsList::class, ['server' => $this->server, 'remote' => true]],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('create')
|
||||
->icon('heroicon-o-plus')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->authorize(fn () => auth()->user()?->can('create', [ServerLog::class, $this->server]))
|
||||
->form([
|
||||
TextInput::make('path')
|
||||
->helperText('The full path of the log file on the server')
|
||||
->rules(fn (callable $get) => CreateServerLog::rules()['path']),
|
||||
])
|
||||
->modalSubmitActionLabel('Create')
|
||||
->action(function (array $data): void {
|
||||
app(CreateServerLog::class)->create($this->server, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Logs\Traits;
|
||||
|
||||
use App\Models\ServerLog;
|
||||
use App\Models\User;
|
||||
use App\Web\Pages\Servers\Logs\Index;
|
||||
use App\Web\Pages\Servers\Logs\RemoteLogs;
|
||||
use Filament\Navigation\NavigationGroup;
|
||||
use Filament\Navigation\NavigationItem;
|
||||
|
||||
trait Navigation
|
||||
{
|
||||
public function getSecondSubNavigation(): array
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
$items = [];
|
||||
|
||||
if ($user->can('viewAny', [ServerLog::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(Index::getNavigationLabel())
|
||||
->icon('heroicon-o-square-3-stack-3d')
|
||||
->isActiveWhen(fn () => request()->routeIs(Index::getRouteName()))
|
||||
->url(Index::getUrl(parameters: ['server' => $this->server]));
|
||||
|
||||
$items[] = NavigationItem::make(RemoteLogs::getNavigationLabel())
|
||||
->icon('heroicon-o-wifi')
|
||||
->isActiveWhen(fn () => request()->routeIs(RemoteLogs::getRouteName()))
|
||||
->url(RemoteLogs::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
return [
|
||||
NavigationGroup::make()
|
||||
->items($items),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Logs\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerLog;
|
||||
use App\Models\Site;
|
||||
use App\Models\User;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\Filter;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class LogsList extends Widget
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public ?Site $site = null;
|
||||
|
||||
public bool $remote = false;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<ServerLog>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return ServerLog::query()
|
||||
->where('server_id', $this->server->id)
|
||||
->where(function (Builder $query): void {
|
||||
if ($this->site instanceof Site) {
|
||||
$query->where('site_id', $this->site->id);
|
||||
}
|
||||
})
|
||||
->where('is_remote', $this->remote);
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('name')
|
||||
->label('Event')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('Created At')
|
||||
->formatStateUsing(fn ($record) => $record->created_at_by_timezone)
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<ServerLog> $query
|
||||
* @return Builder<ServerLog>
|
||||
*/
|
||||
protected function applyDefaultSortingToTableQuery(Builder $query): Builder
|
||||
{
|
||||
return $query->latest('created_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $table
|
||||
->heading(null)
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->filters([
|
||||
Filter::make('created_at')
|
||||
->form([
|
||||
DatePicker::make('created_from'),
|
||||
DatePicker::make('created_until'),
|
||||
])
|
||||
->query(fn (Builder $query, array $data): Builder => $query
|
||||
->when(
|
||||
$data['created_from'],
|
||||
fn (Builder $query, $date): Builder => $query->whereDate('created_at', '>=', $date),
|
||||
)
|
||||
->when(
|
||||
$data['created_until'],
|
||||
fn (Builder $query, $date): Builder => $query->whereDate('created_at', '<=', $date),
|
||||
)),
|
||||
])
|
||||
->actions([
|
||||
Action::make('view')
|
||||
->hiddenLabel()
|
||||
->tooltip('View')
|
||||
->icon('heroicon-o-eye')
|
||||
->authorize(fn ($record) => $user->can('view', $record))
|
||||
->modalHeading('View Log')
|
||||
->modalContent(fn (ServerLog $record) => view('components.console-view', [
|
||||
'slot' => $record->getContent(),
|
||||
'attributes' => new ComponentAttributeBag,
|
||||
]))
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelActionLabel('Close'),
|
||||
Action::make('download')
|
||||
->hiddenLabel()
|
||||
->tooltip('Download')
|
||||
->color('gray')
|
||||
->icon('heroicon-o-archive-box-arrow-down')
|
||||
->authorize(fn ($record) => $user->can('view', $record))
|
||||
->action(fn (ServerLog $record): StreamedResponse => $record->download()),
|
||||
DeleteAction::make()
|
||||
->hiddenLabel()
|
||||
->tooltip('Delete')
|
||||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->authorize(fn ($record) => $user->can('delete', $record)),
|
||||
])
|
||||
->bulkActions([
|
||||
DeleteBulkAction::make()
|
||||
->requiresConfirmation()
|
||||
->authorize($user->can('deleteMany', [ServerLog::class, $this->server])),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Metrics;
|
||||
|
||||
use App\Actions\Monitoring\UpdateMetricSettings;
|
||||
use App\Models\Metric;
|
||||
use App\Web\Pages\Servers\Page;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/metrics';
|
||||
|
||||
protected static ?string $title = 'Metrics';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [Metric::class, $this->server]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\FilterForm::class, ['server' => $this->server]],
|
||||
[Widgets\MetricDetails::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('data-retention')
|
||||
->button()
|
||||
->color('gray')
|
||||
->icon('heroicon-o-trash')
|
||||
->label('Data Retention')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->form([
|
||||
Select::make('data_retention')
|
||||
->options([
|
||||
7 => '7 days',
|
||||
14 => '14 days',
|
||||
30 => '30 days',
|
||||
60 => '60 days',
|
||||
90 => '90 days',
|
||||
180 => '180 days',
|
||||
365 => '365 days',
|
||||
])
|
||||
->rules(UpdateMetricSettings::rules()['data_retention'])
|
||||
->label('Data Retention')
|
||||
->default($this->server->monitoring()?->type_data['data_retention'] ?? 30),
|
||||
])
|
||||
->modalSubmitActionLabel('Save')
|
||||
->action(function (array $data): void {
|
||||
app(UpdateMetricSettings::class)->update($this->server, $data);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Data retention updated')
|
||||
->send();
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Metrics\Widgets;
|
||||
|
||||
use App\Actions\Monitoring\GetMetrics;
|
||||
use App\Models\Server;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\ViewField;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Widgets\Widget;
|
||||
|
||||
class FilterForm extends Widget implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static string $view = 'components.form';
|
||||
|
||||
/**
|
||||
* @var ?array<string, mixed>
|
||||
*/
|
||||
public ?array $data = [
|
||||
'period' => '1h',
|
||||
'from' => null,
|
||||
'to' => null,
|
||||
];
|
||||
|
||||
public function updated(string $name, mixed $value): void
|
||||
{
|
||||
if ($value !== 'custom') {
|
||||
$this->dispatch('updateFilters', filters: $this->data);
|
||||
}
|
||||
|
||||
$from = $this->data['from'] ?? null;
|
||||
$to = $this->data['to'] ?? null;
|
||||
|
||||
if ($value === 'custom' && $from && $to) {
|
||||
$this->dispatch('updateFilters', filters: $this->data);
|
||||
}
|
||||
}
|
||||
|
||||
public Server $server;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Grid::make()
|
||||
->columns(3)
|
||||
->schema([
|
||||
Select::make('period')
|
||||
->live()
|
||||
->reactive()
|
||||
->options([
|
||||
'10m' => '10 Minutes',
|
||||
'30m' => '30 Minutes',
|
||||
'1h' => '1 Hour',
|
||||
'12h' => '12 Hours',
|
||||
'1d' => '1 Day',
|
||||
'7d' => '7 Days',
|
||||
'custom' => 'Custom',
|
||||
])
|
||||
->rules(fn (Get $get) => GetMetrics::rules($get())['period']),
|
||||
DatePicker::make('from')
|
||||
->reactive()
|
||||
->visible(fn (Get $get): bool => $get('period') === 'custom')
|
||||
->maxDate(fn (Get $get) => now())
|
||||
->rules(fn (Get $get) => GetMetrics::rules($get())['from']),
|
||||
DatePicker::make('to')
|
||||
->reactive()
|
||||
->visible(fn (Get $get): bool => $get('period') === 'custom')
|
||||
->minDate(fn (Get $get) => $get('from') ?: now())
|
||||
->maxDate(now())
|
||||
->rules(fn (Get $get) => GetMetrics::rules($get())['to']),
|
||||
]),
|
||||
ViewField::make('data')
|
||||
->reactive()
|
||||
->view('components.dynamic-widget', [
|
||||
'widget' => Metrics::class,
|
||||
'params' => [
|
||||
'server' => $this->server,
|
||||
'filters' => $this->data,
|
||||
],
|
||||
]),
|
||||
])
|
||||
->statePath('data');
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Metrics\Widgets;
|
||||
|
||||
use App\Models\Metric;
|
||||
use App\Models\Server;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Infolists\Components\Grid;
|
||||
use Filament\Infolists\Components\Section;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Infolists\Concerns\InteractsWithInfolists;
|
||||
use Filament\Infolists\Contracts\HasInfolists;
|
||||
use Filament\Infolists\Infolist;
|
||||
use Filament\Widgets\Widget;
|
||||
use Illuminate\Support\Number;
|
||||
|
||||
class MetricDetails extends Widget implements HasForms, HasInfolists
|
||||
{
|
||||
use InteractsWithForms;
|
||||
use InteractsWithInfolists;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static string $view = 'components.infolist';
|
||||
|
||||
public Server $server;
|
||||
|
||||
public function infolist(Infolist $infolist): Infolist
|
||||
{
|
||||
return $infolist
|
||||
->record($this->server->metrics()->latest()->first() ?? new Metric)
|
||||
->schema([
|
||||
Grid::make()
|
||||
->schema([
|
||||
Section::make()
|
||||
->heading('Memory')
|
||||
->description('More details on memory')
|
||||
->columnSpan(1)
|
||||
->schema([
|
||||
TextEntry::make('memory_total')
|
||||
->label('Total Memory')
|
||||
->alignRight()
|
||||
->formatStateUsing(fn (Metric $record) => Number::fileSize($record->memory_total_in_bytes, 2))
|
||||
->inlineLabel(),
|
||||
TextEntry::make('memory_used')
|
||||
->label('Used Memory')
|
||||
->alignRight()
|
||||
->formatStateUsing(fn (Metric $record) => Number::fileSize($record->memory_used_in_bytes, 2))
|
||||
->inlineLabel(),
|
||||
TextEntry::make('memory_free')
|
||||
->label('Free Memory')
|
||||
->formatStateUsing(fn (Metric $record) => Number::fileSize($record->memory_free_in_bytes, 2))
|
||||
->alignRight()
|
||||
->inlineLabel(),
|
||||
]),
|
||||
Section::make()
|
||||
->heading('Disk')
|
||||
->description('More details on disk')
|
||||
->columnSpan(1)
|
||||
->schema([
|
||||
TextEntry::make('disk_total')
|
||||
->label('Total Disk')
|
||||
->formatStateUsing(fn (Metric $record) => Number::fileSize($record->disk_total_in_bytes, 2))
|
||||
->alignRight()
|
||||
->inlineLabel(),
|
||||
TextEntry::make('disk_used')
|
||||
->label('Used Disk')
|
||||
->formatStateUsing(fn (Metric $record) => Number::fileSize($record->disk_used_in_bytes, 2))
|
||||
->alignRight()
|
||||
->inlineLabel(),
|
||||
TextEntry::make('disk_free')
|
||||
->label('Free Disk')
|
||||
->formatStateUsing(fn (Metric $record) => Number::fileSize($record->disk_free_in_bytes, 2))
|
||||
->alignRight()
|
||||
->inlineLabel(),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Metrics\Widgets;
|
||||
|
||||
use App\Actions\Monitoring\GetMetrics;
|
||||
use App\Models\Metric;
|
||||
use App\Models\Server;
|
||||
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
use Illuminate\Support\Number;
|
||||
use Livewire\Attributes\On;
|
||||
|
||||
class Metrics extends BaseWidget
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
public array $filters = [];
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $filters
|
||||
*/
|
||||
#[On('updateFilters')]
|
||||
public function updateFilters(array $filters): void
|
||||
{
|
||||
$this->filters = $filters;
|
||||
}
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
/** @var Metric $lastMetric */
|
||||
$lastMetric = $this->server
|
||||
->metrics()
|
||||
->latest()
|
||||
->first();
|
||||
$metrics = app(GetMetrics::class)->filter($this->server, $this->filters);
|
||||
|
||||
return [
|
||||
Stat::make('CPU Load', $lastMetric->load ?? 0)
|
||||
->color('success')
|
||||
->chart($metrics->pluck('load')->toArray()),
|
||||
Stat::make('Memory Usage', Number::fileSize($lastMetric->memory_used_in_bytes ?? 0, 2))
|
||||
->color('warning')
|
||||
->chart($metrics->pluck('memory_used')->toArray()),
|
||||
Stat::make('Disk Usage', Number::fileSize($lastMetric->disk_used_in_bytes ?? 0, 2))
|
||||
->color('primary')
|
||||
->chart($metrics->pluck('disk_used')->toArray()),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\NodeJS;
|
||||
|
||||
use App\Actions\NodeJS\InstallNewNodeJsVersion;
|
||||
use App\Enums\NodeJS;
|
||||
use App\Models\Service;
|
||||
use App\Web\Pages\Servers\NodeJS\Widgets\NodeJSList;
|
||||
use App\Web\Pages\Servers\Page;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/nodejs';
|
||||
|
||||
protected static ?string $title = 'NodeJS';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [Service::class, $this->server]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[NodeJSList::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
$installedNodeVersions = $this->server->installedNodejsVersions();
|
||||
|
||||
return [
|
||||
Action::make('install')
|
||||
->authorize(fn () => auth()->user()?->can('create', [Service::class, $this->server]))
|
||||
->label('Install Node')
|
||||
->icon('heroicon-o-archive-box-arrow-down')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->form([
|
||||
Select::make('version')
|
||||
->options(
|
||||
collect((array) config('core.nodejs_versions'))
|
||||
->filter(fn ($version): bool => ! in_array($version, array_merge($installedNodeVersions, [NodeJS::NONE])))
|
||||
->mapWithKeys(fn ($version) => [$version => $version])
|
||||
->toArray()
|
||||
)
|
||||
->rules(InstallNewNodeJsVersion::rules($this->server)['version']),
|
||||
])
|
||||
->modalSubmitActionLabel('Install')
|
||||
->action(function (array $data): void {
|
||||
app(InstallNewNodeJsVersion::class)->install($this->server, $data);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Installing Node...')
|
||||
->send();
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\NodeJS\Widgets;
|
||||
|
||||
use App\Actions\NodeJS\ChangeDefaultCli;
|
||||
use App\Actions\Service\Uninstall;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use Exception;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class NodeJSList extends Widget
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<Service>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Service::query()->where('type', 'nodejs')->where('server_id', $this->server->id);
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('version')
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (Service $service) => Service::$statusColors[$service->status])
|
||||
->sortable(),
|
||||
TextColumn::make('is_default')
|
||||
->label('Default Cli')
|
||||
->badge()
|
||||
->color(fn (Service $service): string => $service->is_default ? 'primary' : 'gray')
|
||||
->state(fn (Service $service): string => $service->is_default ? 'Yes' : 'No')
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('Installed At')
|
||||
->formatStateUsing(fn ($record) => $record->created_at_by_timezone),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->heading(null)
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->actions([
|
||||
ActionGroup::make([
|
||||
$this->defaultNodeJsCliAction(),
|
||||
$this->uninstallAction(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
private function defaultNodeJsCliAction(): Action
|
||||
{
|
||||
return Action::make('default-nodejs-cli')
|
||||
->authorize(fn (Service $nodejs) => auth()->user()?->can('update', $nodejs))
|
||||
->label('Make Default CLI')
|
||||
->hidden(fn (Service $service) => $service->is_default)
|
||||
->action(function (Service $service): void {
|
||||
try {
|
||||
app(ChangeDefaultCli::class)->change($this->server, ['version' => $service->version]);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Default NodeJS CLI changed!')
|
||||
->send();
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}
|
||||
|
||||
private function uninstallAction(): Action
|
||||
{
|
||||
return Action::make('uninstall')
|
||||
->authorize(fn (Service $nodejs) => auth()->user()?->can('update', $nodejs))
|
||||
->label('Uninstall')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->action(function (Service $service): void {
|
||||
try {
|
||||
app(Uninstall::class)->uninstall($service);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\PHP;
|
||||
|
||||
use App\Actions\PHP\InstallNewPHP;
|
||||
use App\Enums\PHP;
|
||||
use App\Models\Service;
|
||||
use App\Web\Pages\Servers\Page;
|
||||
use App\Web\Pages\Servers\PHP\Widgets\PHPList;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/php';
|
||||
|
||||
protected static ?string $title = 'PHP';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [Service::class, $this->server]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[PHPList::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
$installedPHPs = $this->server->installedPHPVersions();
|
||||
|
||||
return [
|
||||
Action::make('install')
|
||||
->authorize(fn () => auth()->user()?->can('create', [Service::class, $this->server]))
|
||||
->label('Install PHP')
|
||||
->icon('heroicon-o-archive-box-arrow-down')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->form([
|
||||
Select::make('version')
|
||||
->options(
|
||||
collect((array) config('core.php_versions'))
|
||||
->filter(fn ($version): bool => ! in_array($version, array_merge($installedPHPs, [PHP::NONE])))
|
||||
->mapWithKeys(fn ($version) => [$version => $version])
|
||||
->toArray()
|
||||
)
|
||||
->rules(InstallNewPHP::rules($this->server)['version']),
|
||||
])
|
||||
->modalSubmitActionLabel('Install')
|
||||
->action(function (array $data): void {
|
||||
app(InstallNewPHP::class)->install($this->server, $data);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Installing PHP...')
|
||||
->send();
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,252 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\PHP\Widgets;
|
||||
|
||||
use App\Actions\PHP\ChangeDefaultCli;
|
||||
use App\Actions\PHP\GetPHPIni;
|
||||
use App\Actions\PHP\InstallPHPExtension;
|
||||
use App\Actions\PHP\UpdatePHPIni;
|
||||
use App\Actions\Service\Manage;
|
||||
use App\Actions\Service\Uninstall;
|
||||
use App\Enums\PHPIniType;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Web\Pages\Servers\Logs\Index;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class PHPList extends Widget
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<Service>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Service::query()->where('type', 'php')->where('server_id', $this->server->id);
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('version')
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (Service $service) => Service::$statusColors[$service->status])
|
||||
->sortable(),
|
||||
TextColumn::make('is_default')
|
||||
->label('Default Cli')
|
||||
->badge()
|
||||
->color(fn (Service $service): string => $service->is_default ? 'primary' : 'gray')
|
||||
->state(fn (Service $service): string => $service->is_default ? 'Yes' : 'No')
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('Installed At')
|
||||
->formatStateUsing(fn ($record) => $record->created_at_by_timezone),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->heading(null)
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->actions([
|
||||
ActionGroup::make([
|
||||
$this->installExtensionAction(),
|
||||
$this->editPHPIniAction(PHPIniType::CLI),
|
||||
$this->editPHPIniAction(PHPIniType::FPM),
|
||||
$this->defaultPHPCliAction(),
|
||||
$this->restartFPMAction(),
|
||||
$this->uninstallAction(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
private function installExtensionAction(): Action
|
||||
{
|
||||
return Action::make('install-extension')
|
||||
->authorize(fn (Service $php) => auth()->user()?->can('update', $php))
|
||||
->label('Install Extension')
|
||||
->modalHeading('Install PHP Extension')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->modalSubmitActionLabel('Install')
|
||||
->form([
|
||||
Hidden::make('version')
|
||||
->default(fn (Service $service) => $service->version)
|
||||
->rules(InstallPHPExtension::rules($this->server)['version']),
|
||||
Select::make('extension')
|
||||
->options(
|
||||
collect((array) config('core.php_extensions'))
|
||||
->mapWithKeys(fn ($extension) => [$extension => $extension])
|
||||
)
|
||||
->rules(InstallPHPExtension::rules($this->server)['extension']),
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
$this->validate();
|
||||
|
||||
try {
|
||||
app(InstallPHPExtension::class)->install($this->server, $data);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('PHP Extension is being installed!')
|
||||
->body('You can check the logs for more information.')
|
||||
->actions([
|
||||
\Filament\Notifications\Actions\Action::make('View Logs')
|
||||
->url(Index::getUrl(parameters: ['server' => $this->server->id])),
|
||||
])
|
||||
->send();
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}
|
||||
|
||||
private function defaultPHPCliAction(): Action
|
||||
{
|
||||
return Action::make('default-php-cli')
|
||||
->authorize(fn (Service $php) => auth()->user()?->can('update', $php))
|
||||
->label('Make Default CLI')
|
||||
->hidden(fn (Service $service) => $service->is_default)
|
||||
->action(function (Service $service): void {
|
||||
try {
|
||||
app(ChangeDefaultCli::class)->change($this->server, ['version' => $service->version]);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Default PHP CLI changed!')
|
||||
->send();
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}
|
||||
|
||||
private function editPHPIniAction(string $type): Action
|
||||
{
|
||||
return Action::make('php-ini-'.$type)
|
||||
->authorize(fn (Service $php) => auth()->user()?->can('update', $php))
|
||||
->label('Update PHP ini ('.$type.')')
|
||||
->modalWidth(MaxWidth::TwoExtraLarge)
|
||||
->modalSubmitActionLabel('Save')
|
||||
->form([
|
||||
Hidden::make('type')
|
||||
->default($type)
|
||||
->rules(UpdatePHPIni::rules($this->server)['type']),
|
||||
Hidden::make('version')
|
||||
->default(fn (Service $service) => $service->version)
|
||||
->rules(UpdatePHPIni::rules($this->server)['version']),
|
||||
Textarea::make('ini')
|
||||
->label('PHP ini')
|
||||
->rows(20)
|
||||
->rules(UpdatePHPIni::rules($this->server)['ini'])
|
||||
->default(fn (Service $service) => app(GetPHPIni::class)->getIni($this->server, [
|
||||
'type' => $type,
|
||||
'version' => $service->version,
|
||||
])),
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
$this->validate();
|
||||
|
||||
try {
|
||||
app(UpdatePHPIni::class)->update($this->server, $data);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('PHP ini updated!')
|
||||
->body('Restarting PHP...')
|
||||
->send();
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}
|
||||
|
||||
private function restartFPMAction(): Action
|
||||
{
|
||||
return Action::make('restart-fpm')
|
||||
->authorize(fn (Service $php) => auth()->user()?->can('update', $php))
|
||||
->label('Restart PHP FPM')
|
||||
->action(function (Service $service): void {
|
||||
try {
|
||||
app(Manage::class)->restart($service);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}
|
||||
|
||||
private function uninstallAction(): Action
|
||||
{
|
||||
return Action::make('uninstall')
|
||||
->authorize(fn (Service $php) => auth()->user()?->can('update', $php))
|
||||
->label('Uninstall')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->action(function (Service $service): void {
|
||||
try {
|
||||
app(Uninstall::class)->uninstall($service);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers;
|
||||
|
||||
use App\Models\CronJob;
|
||||
use App\Models\Database;
|
||||
use App\Models\FirewallRule;
|
||||
use App\Models\Metric;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerLog;
|
||||
use App\Models\Service;
|
||||
use App\Models\Site;
|
||||
use App\Models\SshKey;
|
||||
use App\Models\User;
|
||||
use App\Models\Worker;
|
||||
use App\Web\Components\Page as BasePage;
|
||||
use App\Web\Pages\Servers\Console\Index as ConsoleIndex;
|
||||
use App\Web\Pages\Servers\CronJobs\Index as CronJobsIndex;
|
||||
use App\Web\Pages\Servers\Databases\Index as DatabasesIndex;
|
||||
use App\Web\Pages\Servers\FileManager\Index as FileManagerIndex;
|
||||
use App\Web\Pages\Servers\Firewall\Index as FirewallIndex;
|
||||
use App\Web\Pages\Servers\Logs\Index as LogsIndex;
|
||||
use App\Web\Pages\Servers\Metrics\Index as MetricsIndex;
|
||||
use App\Web\Pages\Servers\NodeJS\Index as NodeJsIndex;
|
||||
use App\Web\Pages\Servers\PHP\Index as PHPIndex;
|
||||
use App\Web\Pages\Servers\Services\Index as ServicesIndex;
|
||||
use App\Web\Pages\Servers\Settings as ServerSettings;
|
||||
use App\Web\Pages\Servers\Sites\Index as SitesIndex;
|
||||
use App\Web\Pages\Servers\SSHKeys\Index as SshKeysIndex;
|
||||
use App\Web\Pages\Servers\View as ServerView;
|
||||
use App\Web\Pages\Servers\Widgets\ServerSummary;
|
||||
use App\Web\Pages\Servers\Workers\Index as WorkersIndex;
|
||||
use Filament\Navigation\NavigationItem;
|
||||
|
||||
abstract class Page extends BasePage
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
public function getSubNavigation(): array
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
$items = [];
|
||||
|
||||
if ($user->can('view', $this->server)) {
|
||||
$items[] = NavigationItem::make(ServerView::getNavigationLabel())
|
||||
->icon('heroicon-o-chart-pie')
|
||||
->isActiveWhen(fn () => request()->routeIs(ServerView::getRouteName()))
|
||||
->url(ServerView::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('viewAny', [Site::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(SitesIndex::getNavigationLabel())
|
||||
->icon('heroicon-o-cursor-arrow-ripple')
|
||||
->isActiveWhen(fn () => request()->routeIs(SitesIndex::getRouteName().'*'))
|
||||
->url(SitesIndex::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('viewAny', [Database::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(DatabasesIndex::getNavigationLabel())
|
||||
->icon('heroicon-o-circle-stack')
|
||||
->isActiveWhen(fn () => request()->routeIs(DatabasesIndex::getRouteName().'*'))
|
||||
->url(DatabasesIndex::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('manage', $this->server)) {
|
||||
$items[] = NavigationItem::make(FileManagerIndex::getNavigationLabel())
|
||||
->icon('heroicon-o-folder')
|
||||
->isActiveWhen(fn () => request()->routeIs(FileManagerIndex::getRouteName().'*'))
|
||||
->url(FileManagerIndex::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('viewAny', [Service::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(PHPIndex::getNavigationLabel())
|
||||
->icon('icon-php-alt')
|
||||
->isActiveWhen(fn () => request()->routeIs(PHPIndex::getRouteName().'*'))
|
||||
->url(PHPIndex::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('viewAny', [Service::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(NodeJsIndex::getNavigationLabel())
|
||||
->icon('icon-nodejs-alt')
|
||||
->isActiveWhen(fn () => request()->routeIs(NodeJsIndex::getRouteName().'*'))
|
||||
->url(NodeJsIndex::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('viewAny', [FirewallRule::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(FirewallIndex::getNavigationLabel())
|
||||
->icon('heroicon-o-fire')
|
||||
->isActiveWhen(fn () => request()->routeIs(FirewallIndex::getRouteName().'*'))
|
||||
->url(FirewallIndex::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('viewAny', [CronJob::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(CronJobsIndex::getNavigationLabel())
|
||||
->icon('heroicon-o-clock')
|
||||
->isActiveWhen(fn () => request()->routeIs(CronJobsIndex::getRouteName().'*'))
|
||||
->url(CronJobsIndex::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('viewAny', [Worker::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(WorkersIndex::getNavigationLabel())
|
||||
->icon('heroicon-o-queue-list')
|
||||
->isActiveWhen(fn () => request()->routeIs(WorkersIndex::getRouteName().'*'))
|
||||
->url(WorkersIndex::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('viewAnyServer', [SshKey::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(SshKeysIndex::getNavigationLabel())
|
||||
->icon('heroicon-o-key')
|
||||
->isActiveWhen(fn () => request()->routeIs(SshKeysIndex::getRouteName().'*'))
|
||||
->url(SshKeysIndex::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('viewAny', [Service::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(ServicesIndex::getNavigationLabel())
|
||||
->icon('heroicon-o-cog-6-tooth')
|
||||
->isActiveWhen(fn () => request()->routeIs(ServicesIndex::getRouteName().'*'))
|
||||
->url(ServicesIndex::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('viewAny', [Metric::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(MetricsIndex::getNavigationLabel())
|
||||
->icon('heroicon-o-chart-bar')
|
||||
->isActiveWhen(fn () => request()->routeIs(MetricsIndex::getRouteName().'*'))
|
||||
->url(MetricsIndex::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('manage', $this->server)) {
|
||||
$items[] = NavigationItem::make(ConsoleIndex::getNavigationLabel())
|
||||
->icon('heroicon-o-command-line')
|
||||
->isActiveWhen(fn () => request()->routeIs(ConsoleIndex::getRouteName().'*'))
|
||||
->url(ConsoleIndex::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('viewAny', [ServerLog::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(LogsIndex::getNavigationLabel())
|
||||
->icon('heroicon-o-square-3-stack-3d')
|
||||
->isActiveWhen(fn () => request()->routeIs(LogsIndex::getRouteName().'*'))
|
||||
->url(LogsIndex::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if ($user->can('update', $this->server)) {
|
||||
$items[] = NavigationItem::make(ServerSettings::getNavigationLabel())
|
||||
->icon('heroicon-o-wrench-screwdriver')
|
||||
->isActiveWhen(fn () => request()->routeIs(ServerSettings::getRouteName().'*'))
|
||||
->url(ServerSettings::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
protected function getHeaderWidgets(): array
|
||||
{
|
||||
return [
|
||||
ServerSummary::make([
|
||||
'server' => $this->server,
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
public function getHeaderWidgetsColumns(): int|string|array
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\SSHKeys;
|
||||
|
||||
use App\Actions\SshKey\CreateSshKey;
|
||||
use App\Actions\SshKey\DeployKeyToServer;
|
||||
use App\Models\SshKey;
|
||||
use App\Models\User;
|
||||
use App\Web\Pages\Servers\Page;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/ssh-keys';
|
||||
|
||||
protected static ?string $title = 'SSH Keys';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [SshKey::class, $this->server]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\SshKeysList::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return [
|
||||
Action::make('deploy')
|
||||
->label('Deploy a Key')
|
||||
->authorize(fn () => $user->can('createServer', [SshKey::class, $this->server]))
|
||||
->icon('heroicon-o-rocket-launch')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->form([
|
||||
Select::make('type')
|
||||
->options([
|
||||
'existing' => 'An existing key',
|
||||
'new' => 'A new key',
|
||||
])
|
||||
->reactive()
|
||||
->default('existing'),
|
||||
Select::make('key_id')
|
||||
->label('Key')
|
||||
->options($user->sshKeys()->pluck('name', 'id')->toArray())
|
||||
->visible(fn ($get): bool => $get('type') === 'existing')
|
||||
->rules(DeployKeyToServer::rules($user, $this->server)['key_id']),
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->visible(fn ($get): bool => $get('type') === 'new')
|
||||
->rules(CreateSshKey::rules()['name']),
|
||||
Textarea::make('public_key')
|
||||
->label('Public Key')
|
||||
->visible(fn ($get): bool => $get('type') === 'new')
|
||||
->rules(CreateSshKey::rules()['public_key']),
|
||||
])
|
||||
->modalSubmitActionLabel('Deploy')
|
||||
->action(function (array $data) use ($user): void {
|
||||
$this->validate();
|
||||
|
||||
try {
|
||||
if (! isset($data['key_id'])) {
|
||||
$data['key_id'] = app(CreateSshKey::class)->create($user, $data)->id;
|
||||
}
|
||||
|
||||
app(DeployKeyToServer::class)->deploy($this->server, $data);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\SSHKeys\Widgets;
|
||||
|
||||
use App\Actions\SshKey\DeleteKeyFromServer;
|
||||
use App\Models\Server;
|
||||
use App\Models\SshKey;
|
||||
use App\Models\User;
|
||||
use Exception;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class SshKeysList extends TableWidget
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<SshKey>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return SshKey::withTrashed()
|
||||
->whereHas(
|
||||
'servers',
|
||||
fn (Builder $query) => $query->where('server_id', $this->server->id)
|
||||
);
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('name')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
TextColumn::make('user.name')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $table
|
||||
->heading(null)
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->actions([
|
||||
DeleteAction::make('delete')
|
||||
->hiddenLabel()
|
||||
->authorize(fn (SshKey $record) => $user->can('deleteServer', [SshKey::class, $this->server]))
|
||||
->using(function (SshKey $record): void {
|
||||
try {
|
||||
app(DeleteKeyFromServer::class)->delete($this->server, $record);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Services;
|
||||
|
||||
use App\Actions\Service\Install;
|
||||
use App\Models\Service;
|
||||
use App\Web\Pages\Servers\Page;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/services';
|
||||
|
||||
protected static ?string $title = 'Services';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [Service::class, $this->server]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\ServicesList::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
$availableServices = [];
|
||||
foreach (config('core.service_handlers') as $key => $addOn) {
|
||||
if (! $this->server->services()->where('name', $key)->exists()) {
|
||||
$availableServices[$key] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
Action::make('install')
|
||||
->label('Install Service')
|
||||
->icon('heroicon-o-archive-box-arrow-down')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->authorize(fn () => auth()->user()?->can('create', [Service::class, $this->server]))
|
||||
->modalSubmitActionLabel('Install')
|
||||
->form([
|
||||
Select::make('name')
|
||||
->searchable()
|
||||
->options($availableServices)
|
||||
->reactive()
|
||||
->rules(fn ($get) => Install::rules($get())['name']),
|
||||
Select::make('version')
|
||||
->options(function (callable $get) {
|
||||
if (! $get('name')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect((array) config("core.service_versions.{$get('name')}"))
|
||||
->mapWithKeys(fn ($version) => [$version => $version]);
|
||||
})
|
||||
->rules(fn ($get) => Install::rules($get())['version'])
|
||||
->reactive(),
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
$this->validate();
|
||||
|
||||
try {
|
||||
app(Install::class)->install($this->server, $data);
|
||||
|
||||
$this->redirect(self::getUrl(['server' => $this->server]));
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Services\Widgets;
|
||||
|
||||
use App\Actions\Service\Manage;
|
||||
use App\Actions\Service\Uninstall;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Web\Pages\Servers\Services\Index;
|
||||
use Exception;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ServicesList extends TableWidget
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<Service>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Service::query()->where('server_id', $this->server->id);
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
IconColumn::make('id')
|
||||
->label('Service')
|
||||
->icon(fn (Service $record): string => 'icon-'.$record->name)
|
||||
->width(24),
|
||||
TextColumn::make('name')
|
||||
->sortable(),
|
||||
TextColumn::make('version')
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (Service $service) => Service::$statusColors[$service->status])
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('Installed At')
|
||||
->formatStateUsing(fn ($record) => $record->created_at_by_timezone)
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->heading('Installed Services')
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->actions([
|
||||
ActionGroup::make([
|
||||
$this->serviceAction('start', 'heroicon-o-play'),
|
||||
$this->serviceAction('stop', 'heroicon-o-stop'),
|
||||
$this->serviceAction('restart', 'heroicon-o-arrow-path'),
|
||||
$this->serviceAction('disable', 'heroicon-o-x-mark'),
|
||||
$this->serviceAction('enable', 'heroicon-o-check'),
|
||||
$this->uninstallAction(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
private function serviceAction(string $type, string $icon): Action
|
||||
{
|
||||
return Action::make($type)
|
||||
->authorize(fn (Service $service) => auth()->user()?->can($type, $service))
|
||||
->label(ucfirst($type).' Service')
|
||||
->icon($icon)
|
||||
->action(function (Service $service) use ($type): void {
|
||||
try {
|
||||
app(Manage::class)->$type($service);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}
|
||||
|
||||
private function uninstallAction(): Action
|
||||
{
|
||||
return Action::make('uninstall')
|
||||
->authorize(fn (Service $service) => auth()->user()?->can('delete', $service))
|
||||
->label('Uninstall Service')
|
||||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->action(function (Service $service): void {
|
||||
try {
|
||||
app(Uninstall::class)->uninstall($service);
|
||||
|
||||
$this->redirect(Index::getUrl(['server' => $this->server]));
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers;
|
||||
|
||||
use App\Actions\Server\RebootServer;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Web\Pages\Servers\Widgets\ServerDetails;
|
||||
use App\Web\Pages\Servers\Widgets\UpdateServerInfo;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Throwable;
|
||||
|
||||
class Settings extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/settings';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static ?string $title = 'Settings';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
$this->authorize('update', [$this->server, $user->currentProject]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
ServerDetails::class, ['server' => $this->server],
|
||||
],
|
||||
[
|
||||
UpdateServerInfo::class, ['server' => $this->server],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('delete')
|
||||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Delete Server')
|
||||
->modalDescription('Once your server is deleted, all of its resources and data will be permanently deleted and can\'t be restored')
|
||||
->authorize('delete', $this->server)
|
||||
->action(function (): void {
|
||||
try {
|
||||
$this->server->delete();
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Server deleted')
|
||||
->send();
|
||||
|
||||
$this->redirect(Index::getUrl());
|
||||
} catch (Throwable $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
Action::make('reboot')
|
||||
->color('gray')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->label('Reboot')
|
||||
->requiresConfirmation()
|
||||
->action(function (): void {
|
||||
app(RebootServer::class)->reboot($this->server);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->info()
|
||||
->title('Server is being rebooted')
|
||||
->send();
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getServer(): ?Server
|
||||
{
|
||||
return $this->server;
|
||||
}
|
||||
}
|
@ -1,218 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites;
|
||||
|
||||
use App\Actions\Site\CreateSite;
|
||||
use App\Enums\LoadBalancerMethod;
|
||||
use App\Enums\SiteType;
|
||||
use App\Models\Site;
|
||||
use App\Models\SourceControl;
|
||||
use App\Models\User;
|
||||
use App\Web\Pages\Settings\SourceControls\Actions\Create;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\Grid;
|
||||
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 \App\Web\Pages\Servers\Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/sites';
|
||||
|
||||
protected static ?string $title = 'Sites';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [Site::class, $this->server]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\SitesList::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return [
|
||||
Action::make('read-the-docs')
|
||||
->label('Read the Docs')
|
||||
->icon('heroicon-o-document-text')
|
||||
->color('gray')
|
||||
->url('https://vitodeploy.com/sites/site-types')
|
||||
->openUrlInNewTab(),
|
||||
Action::make('create')
|
||||
->label('Create a Site')
|
||||
->icon('heroicon-o-plus')
|
||||
->authorize(fn () => $user->can('create', [Site::class, $this->server]))
|
||||
->modalWidth(MaxWidth::FiveExtraLarge)
|
||||
->slideOver()
|
||||
->form([
|
||||
Select::make('type')
|
||||
->label('Site Type')
|
||||
->options(
|
||||
collect((array) config('core.site_types'))->mapWithKeys(fn ($type) => [$type => $type])
|
||||
)
|
||||
->reactive()
|
||||
->afterStateUpdated(function (?string $state, Set $set): void {
|
||||
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): bool => 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): bool => 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($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 () => $user->can('create', SourceControl::class))
|
||||
->action(fn (array $data) => Create::action($data))
|
||||
)
|
||||
->placeholder('Select source control')
|
||||
->live()
|
||||
->visible(fn (Get $get): bool => 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): bool => 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): bool => isset(CreateSite::rules($this->server, $get())['branch'])),
|
||||
Checkbox::make('composer')
|
||||
->label('Run `composer install --no-dev`')
|
||||
->default(false)
|
||||
->visible(fn (Get $get): bool => isset(CreateSite::rules($this->server, $get())['composer'])),
|
||||
// PHPMyAdmin
|
||||
Select::make('version')
|
||||
->label('PHPMyAdmin Version')
|
||||
->validationAttribute('PHPMyAdmin Version')
|
||||
->options([
|
||||
'5.2.1' => '5.2.1',
|
||||
])
|
||||
->visible(fn (Get $get): bool => $get('type') === SiteType::PHPMYADMIN)
|
||||
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['version']),
|
||||
// WordPress
|
||||
$this->wordpressFields(),
|
||||
// Load Balancer
|
||||
Select::make('method')
|
||||
->label('Balancing Method')
|
||||
->validationAttribute('Balancing Method')
|
||||
->options(
|
||||
collect(LoadBalancerMethod::all())
|
||||
->mapWithKeys(fn ($method) => [$method => $method])
|
||||
)
|
||||
->visible(fn (Get $get): bool => $get('type') === SiteType::LOAD_BALANCER)
|
||||
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['method'] ?? []),
|
||||
// User
|
||||
TextInput::make('user')
|
||||
->label('User')
|
||||
->placeholder('vito')
|
||||
->helperText(
|
||||
'Optional. If provided, a new user will be created and the site will be owned by this user.'
|
||||
)
|
||||
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['user']),
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
$this->authorize('create', [Site::class, $this->server]);
|
||||
|
||||
$this->validate();
|
||||
|
||||
try {
|
||||
$site = app(CreateSite::class)->create($this->server, $data);
|
||||
|
||||
$this->redirect(View::getUrl([
|
||||
'server' => $this->server,
|
||||
'site' => $site,
|
||||
]));
|
||||
} catch (Throwable $e) {
|
||||
Notification::make()
|
||||
->title($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
})
|
||||
->modalSubmitActionLabel('Create Site'),
|
||||
];
|
||||
}
|
||||
|
||||
private function wordpressFields(): Component
|
||||
{
|
||||
/** @var User */
|
||||
$user = auth()->user();
|
||||
|
||||
return Grid::make()
|
||||
->columns(3)
|
||||
->visible(fn (Get $get): bool => $get('type') === SiteType::WORDPRESS)
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label('Site Title')
|
||||
->columnSpan(3)
|
||||
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['title']),
|
||||
TextInput::make('email')
|
||||
->label('WP Admin Email')
|
||||
->default($user->email)
|
||||
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['email']),
|
||||
TextInput::make('username')
|
||||
->label('WP Admin Username')
|
||||
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['username']),
|
||||
TextInput::make('password')
|
||||
->label('WP Admin Password')
|
||||
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['password']),
|
||||
TextInput::make('database')
|
||||
->helperText('It will create a database with this name')
|
||||
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['database']),
|
||||
\App\Web\Pages\Servers\Databases\Index::getCharsetInput($this->server),
|
||||
\App\Web\Pages\Servers\Databases\Index::getCollationInput($this->server),
|
||||
TextInput::make('database_user')
|
||||
->helperText('It will create a db user with this username')
|
||||
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['database']),
|
||||
TextInput::make('database_password')
|
||||
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['database']),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites;
|
||||
|
||||
use App\Models\Redirect;
|
||||
use App\Models\ServerLog;
|
||||
use App\Models\Site;
|
||||
use App\Models\Ssl;
|
||||
use App\Models\User;
|
||||
use App\Models\Worker;
|
||||
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
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
$items = [];
|
||||
|
||||
if ($user->can('view', [$this->site, $this->server])) {
|
||||
$items[] = NavigationItem::make(View::getNavigationLabel())
|
||||
->icon('icon-'.$this->site->type)
|
||||
->isActiveWhen(fn () => request()->routeIs(View::getRouteName()))
|
||||
->url(View::getUrl(parameters: [
|
||||
'server' => $this->server,
|
||||
'site' => $this->site,
|
||||
]));
|
||||
}
|
||||
|
||||
if ($user->can('viewAny', [Ssl::class, $this->site, $this->server])) {
|
||||
$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 ($user->can('viewAny', [Worker::class, $this->server, $this->site])) {
|
||||
$items[] = NavigationItem::make(Pages\Workers\Index::getNavigationLabel())
|
||||
->icon('heroicon-o-queue-list')
|
||||
->isActiveWhen(fn () => request()->routeIs(Pages\Workers\Index::getRouteName()))
|
||||
->url(Pages\Workers\Index::getUrl(parameters: [
|
||||
'server' => $this->server,
|
||||
'site' => $this->site,
|
||||
]));
|
||||
}
|
||||
|
||||
if ($user->can('viewAny', [ServerLog::class, $this->server])) {
|
||||
$items[] = NavigationItem::make(Pages\Logs\Index::getNavigationLabel())
|
||||
->icon('heroicon-o-square-3-stack-3d')
|
||||
->isActiveWhen(fn () => request()->routeIs(Pages\Logs\Index::getRouteName()))
|
||||
->url(Pages\Logs\Index::getUrl(parameters: [
|
||||
'server' => $this->server,
|
||||
'site' => $this->site,
|
||||
]));
|
||||
}
|
||||
|
||||
if ($user->can('update', [$this->site, $this->server])) {
|
||||
$items[] = NavigationItem::make(Settings::getNavigationLabel())
|
||||
->icon('heroicon-o-wrench-screwdriver')
|
||||
->isActiveWhen(fn () => request()->routeIs(Settings::getRouteName()))
|
||||
->url(Settings::getUrl(parameters: [
|
||||
'server' => $this->server,
|
||||
'site' => $this->site,
|
||||
]));
|
||||
}
|
||||
|
||||
if ($user->can('view', [Redirect::class, $this->site, $this->server])) {
|
||||
$items[] = NavigationItem::make(Pages\Redirects\Index::getNavigationLabel())
|
||||
->icon('heroicon-o-arrows-right-left')
|
||||
->isActiveWhen(fn () => request()->routeIs(Pages\Redirects\Index::getRouteName()))
|
||||
->url(Pages\Redirects\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) {
|
||||
/** @var Site $site */
|
||||
$site = Site::query()->findOrFail($site);
|
||||
|
||||
return $site;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Pages\Logs;
|
||||
|
||||
use App\Models\ServerLog;
|
||||
use App\Web\Pages\Servers\Logs\Widgets\LogsList;
|
||||
use App\Web\Pages\Servers\Sites\Page;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/sites/{site}/logs';
|
||||
|
||||
protected static ?string $title = 'Logs';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [ServerLog::class, $this->server]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
LogsList::class, [
|
||||
'server' => $this->server,
|
||||
'site' => $this->site,
|
||||
'label' => 'Logs',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Pages\Redirects\Actions;
|
||||
|
||||
use App\Actions\Redirect\CreateRedirect;
|
||||
use App\Models\Site;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Livewire\Component;
|
||||
|
||||
class Create
|
||||
{
|
||||
/**
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
public static function form(Site $site): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('from')
|
||||
->rules(CreateRedirect::rules($site)['from']),
|
||||
TextInput::make('to')
|
||||
->rules(CreateRedirect::rules($site)['to']),
|
||||
Select::make('mode')
|
||||
->rules(CreateRedirect::rules($site)['mode'])
|
||||
->options([
|
||||
'301' => '301 - Moved Permanently',
|
||||
'302' => '302 - Found',
|
||||
'307' => '307 - Temporary Redirect',
|
||||
'308' => '308 - Permanent Redirect',
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function action(Component $component, array $data, Site $site): void
|
||||
{
|
||||
app(CreateRedirect::class)->create($site, $data);
|
||||
|
||||
$component->dispatch('$refresh');
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Pages\Redirects;
|
||||
|
||||
use App\Models\Redirect;
|
||||
use App\Web\Pages\Servers\Sites\Page;
|
||||
use App\Web\Pages\Servers\Sites\Pages\Redirects\Actions\Create;
|
||||
use App\Web\Pages\Servers\Sites\Pages\Redirects\Widgets\RedirectsList;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/sites/{site}/redirects';
|
||||
|
||||
protected static ?string $title = 'Redirects';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('view', [Redirect::class, $this->site, $this->server]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
RedirectsList::class, [
|
||||
'site' => $this->site,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('read-the-docs')
|
||||
->label('Read the Docs')
|
||||
->icon('heroicon-o-document-text')
|
||||
->color('gray')
|
||||
->url('https://vitodeploy.com/docs/sites/redirects')
|
||||
->openUrlInNewTab(),
|
||||
CreateAction::make('create')
|
||||
->icon('heroicon-o-plus')
|
||||
->createAnother(false)
|
||||
->modalWidth(MaxWidth::ExtraLarge)
|
||||
->label('New Redirect')
|
||||
->form(Create::form($this->site))
|
||||
->using(fn (array $data) => run_action($this, function () use ($data): void {
|
||||
Create::action($this, $data, $this->site);
|
||||
})),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Pages\Redirects\Widgets;
|
||||
|
||||
use App\Actions\Redirect\DeleteRedirect;
|
||||
use App\Models\Redirect;
|
||||
use App\Models\Site;
|
||||
use App\Models\User;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class RedirectsList extends Widget
|
||||
{
|
||||
public Site $site;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<Redirect>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Redirect::query()->where('site_id', $this->site->id);
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
auth()->user();
|
||||
|
||||
return [
|
||||
TextColumn::make('from')
|
||||
->limit(40)
|
||||
->tooltip(fn (Redirect $redirect) => $redirect->from)
|
||||
->searchable()
|
||||
->copyable(),
|
||||
TextColumn::make('to')
|
||||
->limit(40)
|
||||
->tooltip(fn (Redirect $redirect) => $redirect->to)
|
||||
->searchable()
|
||||
->copyable(),
|
||||
TextColumn::make('mode')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (Redirect $redirect) => Redirect::$statusColors[$redirect->status])
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->formatStateUsing(fn (Redirect $record) => $record->created_at)
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $table
|
||||
->heading(null)
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->actions([
|
||||
DeleteAction::make('delete')
|
||||
->hiddenLabel()
|
||||
->tooltip('Delete')
|
||||
->icon('heroicon-o-trash')
|
||||
->authorize(fn (Redirect $record) => $user->can('delete', [$this->site, $this->site->server]))
|
||||
->using(function (Redirect $record): void {
|
||||
run_action($this, function () use ($record): void {
|
||||
app(DeleteRedirect::class)->delete($this->site, $record);
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Pages\SSL;
|
||||
|
||||
use App\Actions\SSL\CreateSSL;
|
||||
use App\Enums\SslType;
|
||||
use App\Models\Ssl;
|
||||
use App\Web\Fields\AlertField;
|
||||
use App\Web\Pages\Servers\Sites\Page;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/sites/{site}/ssl';
|
||||
|
||||
protected static ?string $title = 'SSL';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [Ssl::class, $this->site, $this->server]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\SslsList::class, ['site' => $this->site]],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('read-the-docs')
|
||||
->label('Read the Docs')
|
||||
->icon('heroicon-o-document-text')
|
||||
->color('gray')
|
||||
->url('https://vitodeploy.com/sites/ssl')
|
||||
->openUrlInNewTab(),
|
||||
Action::make('force-ssl')
|
||||
->label('Force SSL')
|
||||
->tooltip(fn (): string => $this->site->force_ssl ? 'Disable force SSL' : 'Enable force SSL')
|
||||
->icon(fn (): string => $this->site->force_ssl ? 'icon-force-ssl-enabled' : 'icon-force-ssl-disabled')
|
||||
->requiresConfirmation()
|
||||
->modalSubmitActionLabel(fn (): string => $this->site->force_ssl ? 'Disable' : 'Enable')
|
||||
->action(function (): void {
|
||||
$this->site->update([
|
||||
'force_ssl' => ! $this->site->force_ssl,
|
||||
]);
|
||||
$this->site->webserver()->updateVHost($this->site);
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('SSL status has been updated.')
|
||||
->send();
|
||||
$this->dispatch('$refresh');
|
||||
})
|
||||
->color('gray'),
|
||||
CreateAction::make('create')
|
||||
->label('New Certificate')
|
||||
->icon('heroicon-o-lock-closed')
|
||||
->form([
|
||||
AlertField::make('letsencrypt-info')
|
||||
->warning()
|
||||
->message('Let\'s Encrypt has rate limits. Read more about them <a href="https://letsencrypt.org/docs/rate-limits/" target="_blank" class="underline">here</a>.'),
|
||||
Select::make('type')
|
||||
->options(
|
||||
collect((array) config('core.ssl_types'))->mapWithKeys(fn ($type) => [$type => $type])
|
||||
)
|
||||
->rules(fn (Get $get) => CreateSSL::rules($get())['type'])
|
||||
->reactive(),
|
||||
TextInput::make('email')
|
||||
->rules(fn (Get $get) => CreateSSL::rules($get())['email'] ?? [])
|
||||
->visible(fn (Get $get): bool => $get('type') === SslType::LETSENCRYPT)
|
||||
->helperText('Email address to provide to Certbot.'),
|
||||
Textarea::make('certificate')
|
||||
->rows(5)
|
||||
->rules(fn (Get $get) => CreateSSL::rules($get())['certificate'])
|
||||
->visible(fn (Get $get): bool => $get('type') === SslType::CUSTOM),
|
||||
Textarea::make('private')
|
||||
->label('Private Key')
|
||||
->rows(5)
|
||||
->rules(fn (Get $get) => CreateSSL::rules($get())['private'])
|
||||
->visible(fn (Get $get): bool => $get('type') === SslType::CUSTOM),
|
||||
DatePicker::make('expires_at')
|
||||
->format('Y-m-d')
|
||||
->rules(fn (Get $get) => CreateSSL::rules($get())['expires_at'])
|
||||
->visible(fn (Get $get): bool => $get('type') === SslType::CUSTOM),
|
||||
Checkbox::make('aliases')
|
||||
->label("Set SSL for site's aliases as well"),
|
||||
])
|
||||
->createAnother(false)
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->using(function (array $data): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
app(CreateSSL::class)->create($this->site, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Pages\SSL\Widgets;
|
||||
|
||||
use App\Actions\SSL\ActivateSSL;
|
||||
use App\Actions\SSL\DeleteSSL;
|
||||
use App\Models\Site;
|
||||
use App\Models\Ssl;
|
||||
use App\Models\User;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
class SslsList extends Widget
|
||||
{
|
||||
public Site $site;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<Ssl>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Ssl::query()->where('site_id', $this->site->id);
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
auth()->user();
|
||||
|
||||
return [
|
||||
IconColumn::make('is_active')
|
||||
->color(fn (Ssl $record): string => $record->is_active ? 'green' : 'gray')
|
||||
->icon(fn (Ssl $record): string => $record->is_active ? 'heroicon-o-lock-closed' : 'heroicon-o-lock-open'),
|
||||
TextColumn::make('type')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->formatStateUsing(fn (Ssl $record) => $record->created_at_by_timezone)
|
||||
->sortable(),
|
||||
TextColumn::make('expires_at')
|
||||
->formatStateUsing(fn (Ssl $record): string => $record->getDateTimeByTimezone($record->expires_at))
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (Ssl $record) => Ssl::$statusColors[$record->status])
|
||||
->searchable()
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $table
|
||||
->heading(null)
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->actions([
|
||||
Action::make('activate-ssl')
|
||||
->hiddenLabel()
|
||||
->visible(fn (Ssl $record): bool => ! $record->is_active)
|
||||
->tooltip('Activate SSL')
|
||||
->icon('heroicon-o-lock-closed')
|
||||
->authorize(fn (Ssl $record) => $user->can('update', [$record->site, $this->site->server]))
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Activate SSL')
|
||||
->modalSubmitActionLabel('Activate')
|
||||
->action(function (Ssl $record): void {
|
||||
run_action($this, function () use ($record): void {
|
||||
app(ActivateSSL::class)->activate($record);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('SSL has been activated.')
|
||||
->send();
|
||||
});
|
||||
}),
|
||||
Action::make('logs')
|
||||
->hiddenLabel()
|
||||
->tooltip('Logs')
|
||||
->icon('heroicon-o-eye')
|
||||
->authorize(fn (Ssl $record) => $user->can('view', [$record, $this->site, $this->site->server]))
|
||||
->modalHeading('View Log')
|
||||
->modalContent(fn (Ssl $record) => view('components.console-view', [
|
||||
'slot' => $record->log?->getContent(),
|
||||
'attributes' => new ComponentAttributeBag,
|
||||
]))
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelActionLabel('Close'),
|
||||
DeleteAction::make('delete')
|
||||
->hiddenLabel()
|
||||
->tooltip('Delete')
|
||||
->icon('heroicon-o-trash')
|
||||
->authorize(fn (Ssl $record) => $user->can('delete', [$record, $this->site, $this->site->server]))
|
||||
->using(function (Ssl $record): void {
|
||||
run_action($this, function () use ($record): void {
|
||||
app(DeleteSSL::class)->delete($record);
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Pages\Workers;
|
||||
|
||||
use App\Models\Worker;
|
||||
use App\Web\Pages\Servers\Sites\Page;
|
||||
use App\Web\Pages\Servers\Workers\Actions\Create;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/sites/{site}/workers';
|
||||
|
||||
protected static ?string $title = 'Workers';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('viewAny', [Worker::class, $this->server, $this->site]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\WorkersList::class, [
|
||||
'server' => $this->server,
|
||||
'site' => $this->site,
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('read-the-docs')
|
||||
->label('Read the Docs')
|
||||
->icon('heroicon-o-document-text')
|
||||
->color('gray')
|
||||
->url('https://vitodeploy.com/servers/workers')
|
||||
->openUrlInNewTab(),
|
||||
CreateAction::make('create')
|
||||
->icon('heroicon-o-plus')
|
||||
->createAnother(false)
|
||||
->modalWidth(MaxWidth::ExtraLarge)
|
||||
->label('New Worker')
|
||||
->form(Create::form($this->server, $this->site))
|
||||
->using(fn (array $data) => run_action($this, function () use ($data): void {
|
||||
Create::action($this, $data, $this->server, $this->site);
|
||||
})),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Pages\Workers\Widgets;
|
||||
|
||||
class WorkersList extends \App\Web\Pages\Servers\Workers\Widgets\WorkersList {}
|
@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites;
|
||||
|
||||
use App\Actions\Site\DeleteSite;
|
||||
use App\Web\Fields\CodeEditorField;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Notifications\Notification;
|
||||
|
||||
class Settings extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}/sites/{site}/settings';
|
||||
|
||||
protected static ?string $title = 'Settings';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('update', [$this->site, $this->server]);
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\SiteDetails::class, ['site' => $this->site]],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->vhostAction(),
|
||||
$this->deleteAction(),
|
||||
];
|
||||
}
|
||||
|
||||
private function deleteAction(): Action
|
||||
{
|
||||
return DeleteAction::make()
|
||||
->icon('heroicon-o-trash')
|
||||
->record($this->site)
|
||||
->modalHeading('Delete Site')
|
||||
->modalDescription('Once your site is deleted, all of its resources and data will be permanently deleted and can\'t be restored')
|
||||
->using(function (): void {
|
||||
run_action($this, function (): void {
|
||||
app(DeleteSite::class)->delete($this->site);
|
||||
|
||||
$this->redirect(Index::getUrl(['server' => $this->server]));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private function vhostAction(): Action
|
||||
{
|
||||
return Action::make('vhost')
|
||||
->color('gray')
|
||||
->icon('icon-nginx')
|
||||
->label('VHost')
|
||||
->modalSubmitActionLabel('Save')
|
||||
->form([
|
||||
CodeEditorField::make('vhost')
|
||||
->formatStateUsing(fn (): string => $this->site->webserver()->getVhost($this->site))
|
||||
->rules(['required']),
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
$this->site->webserver()->updateVHost($this->site, $data['vhost']);
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('VHost updated!')
|
||||
->send();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -1,231 +0,0 @@
|
||||
<?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\Enums\SiteType;
|
||||
use App\Web\Fields\CodeEditorField;
|
||||
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->authorize('view', [$this->site, $this->server]);
|
||||
|
||||
$this->previousStatus = $this->site->status;
|
||||
}
|
||||
|
||||
#[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 ($this->site->isReady()) {
|
||||
if (in_array(SiteFeature::COMMANDS, $this->site->type()->supportedFeatures())) {
|
||||
$widgets[] = [Widgets\Commands::class, ['site' => $this->site]];
|
||||
}
|
||||
|
||||
if (in_array(SiteFeature::DEPLOYMENT, $this->site->type()->supportedFeatures())) {
|
||||
$widgets[] = [Widgets\DeploymentsList::class, ['site' => $this->site]];
|
||||
}
|
||||
|
||||
if ($this->site->type === SiteType::LOAD_BALANCER) {
|
||||
$widgets[] = [Widgets\LoadBalancerServers::class, ['site' => $this->site]];
|
||||
}
|
||||
}
|
||||
|
||||
return $widgets;
|
||||
}
|
||||
|
||||
public function getHeaderActions(): array
|
||||
{
|
||||
$actions = [
|
||||
Action::make('read-the-docs')
|
||||
->label('Read the Docs')
|
||||
->icon('heroicon-o-document-text')
|
||||
->color('gray')
|
||||
->url('https://vitodeploy.com/sites/application')
|
||||
->openUrlInNewTab(),
|
||||
];
|
||||
$actionsGroup = [];
|
||||
|
||||
if (in_array(SiteFeature::DEPLOYMENT, $this->site->type()->supportedFeatures())) {
|
||||
$actions[] = $this->deployAction();
|
||||
if ($this->site->sourceControl) {
|
||||
$actionsGroup[] = $this->autoDeploymentAction();
|
||||
}
|
||||
$actionsGroup[] = $this->deploymentScriptAction();
|
||||
}
|
||||
|
||||
if (in_array(SiteFeature::ENV, $this->site->type()->supportedFeatures())) {
|
||||
$actionsGroup[] = $this->dotEnvAction();
|
||||
}
|
||||
|
||||
if ($this->site->sourceControl) {
|
||||
$actionsGroup[] = $this->branchAction();
|
||||
}
|
||||
|
||||
$actions[] = ActionGroup::make($actionsGroup)
|
||||
->button()
|
||||
->color('gray')
|
||||
->icon('heroicon-o-chevron-up-down')
|
||||
->iconPosition(IconPosition::After)
|
||||
->dropdownPlacement('bottom-end');
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
private function deployAction(): Action
|
||||
{
|
||||
return Action::make('deploy')
|
||||
->icon('heroicon-o-rocket-launch')
|
||||
->action(function (): void {
|
||||
if (! $this->site->deploymentScript?->content) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Deployment script is not set!')
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
run_action($this, function (): void {
|
||||
app(Deploy::class)->run($this->site);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Deployment started!')
|
||||
->send();
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private function autoDeploymentAction(): Action
|
||||
{
|
||||
return Action::make('auto-deployment')
|
||||
->label(fn (): string => $this->site->isAutoDeployment() ? 'Disable Auto Deployment' : 'Enable Auto Deployment')
|
||||
->modalHeading(fn (): string => $this->site->isAutoDeployment() ? 'Disable Auto Deployment' : 'Enable Auto Deployment')
|
||||
->modalIconColor(fn (): string => $this->site->isAutoDeployment() ? 'red' : 'green')
|
||||
->requiresConfirmation()
|
||||
->action(function (): void {
|
||||
run_action($this, function (): void {
|
||||
$this->site->isAutoDeployment()
|
||||
? $this->site->disableAutoDeployment()
|
||||
: $this->site->enableAutoDeployment();
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Auto deployment '.($this->site->isAutoDeployment() ? 'disabled' : 'enabled').'!')
|
||||
->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): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
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(fn (): string => $this->site->getEnv())
|
||||
->rules([
|
||||
'env' => 'required',
|
||||
]),
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
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): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
app(UpdateBranch::class)->update($this->site, $data);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Branch updated!')
|
||||
->send();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Widgets;
|
||||
|
||||
use App\Actions\Site\CreateCommand;
|
||||
use App\Actions\Site\EditCommand;
|
||||
use App\Actions\Site\ExecuteCommand;
|
||||
use App\Models\Command;
|
||||
use App\Models\CommandExecution;
|
||||
use App\Models\Site;
|
||||
use App\Models\User;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
class Commands extends Widget
|
||||
{
|
||||
public Site $site;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<Command>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Command::query()->where('site_id', $this->site->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<Command> $query
|
||||
* @return Builder<Command>
|
||||
*/
|
||||
protected function applySortingToTableQuery(Builder $query): Builder
|
||||
{
|
||||
return $query->latest('created_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('name'),
|
||||
TextColumn::make('lastExecution.status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (Command $record) => CommandExecution::$statusColors[$record->lastExecution?->status])
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('Last Execution At')
|
||||
->formatStateUsing(fn (Command $record) => $record->lastExecution?->created_at_by_timezone)
|
||||
->searchable()
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getTableHeaderActions(): array
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return [
|
||||
Action::make('new-command')
|
||||
->label('Create a Command')
|
||||
->modalDescription('The command will be executed inside the site\'s directory')
|
||||
->icon('heroicon-o-plus')
|
||||
->authorize(fn () => $user->can('create', [Command::class, $this->site, $this->site->server]))
|
||||
->action(function (array $data): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
app(CreateCommand::class)->create($this->site, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Command created!')
|
||||
->send();
|
||||
});
|
||||
})
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->rules(CreateCommand::rules()['name']),
|
||||
TextInput::make('command')
|
||||
->placeholder('php artisan my:command')
|
||||
->rules(CreateCommand::rules()['command'])
|
||||
->helperText('You can use variables like ${VARIABLE_NAME} in the command. The variables will be asked when executing the command'),
|
||||
])
|
||||
->modalSubmitActionLabel('Create')
|
||||
->modalHeading('New Command')
|
||||
->modalWidth('md'),
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $table
|
||||
->query($this->getTableQuery())
|
||||
->headerActions($this->getTableHeaderActions())
|
||||
->columns($this->getTableColumns())
|
||||
->heading('Commands')
|
||||
->defaultPaginationPageOption(5)
|
||||
->searchable(false)
|
||||
->actions([
|
||||
Action::make('execute')
|
||||
->hiddenLabel()
|
||||
->tooltip('Execute')
|
||||
->icon('heroicon-o-play')
|
||||
->modalWidth(MaxWidth::Medium)
|
||||
->modalSubmitActionLabel('Execute')
|
||||
->form(function (Command $record) {
|
||||
$form = [
|
||||
TextInput::make('command')->default($record->command)->disabled(),
|
||||
];
|
||||
|
||||
foreach ($record->getVariables() as $variable) {
|
||||
$form[] = TextInput::make('variables.'.$variable)
|
||||
->label($variable)
|
||||
->rules(fn (Get $get) => ExecuteCommand::rules($get())['variables.*']);
|
||||
}
|
||||
|
||||
return $form;
|
||||
})
|
||||
->authorize(fn (Command $record) => $user->can('update', [$record->site, $record->site->server]))
|
||||
->action(function (array $data, Command $record) use ($user): void {
|
||||
app(ExecuteCommand::class)->execute($record, $user, $data);
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
Action::make('logs')
|
||||
->hiddenLabel()
|
||||
->tooltip('Last Log')
|
||||
->icon('heroicon-o-eye')
|
||||
->modalHeading('View Last Execution Log')
|
||||
->modalContent(fn (Command $record) => view('components.console-view', [
|
||||
'slot' => $record->lastExecution?->serverLog?->getContent() ?? 'Not executed yet',
|
||||
'attributes' => new ComponentAttributeBag,
|
||||
]))
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelActionLabel('Close'),
|
||||
EditAction::make('edit')
|
||||
->hiddenLabel()
|
||||
->tooltip('Edit')
|
||||
->modalHeading('Edit Command')
|
||||
->mutateRecordDataUsing(fn (array $data, Command $record): array => [
|
||||
'name' => $record->name,
|
||||
'command' => $record->command,
|
||||
])
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->rules(EditCommand::rules()['name']),
|
||||
TextInput::make('command')
|
||||
->rules(EditCommand::rules()['command'])
|
||||
->helperText('You can use variables like ${VARIABLE_NAME} in the command. The variables will be asked when executing the command'),
|
||||
|
||||
])
|
||||
->authorize(fn (Command $record) => $user->can('update', [$record, $this->site, $this->site->server]))
|
||||
->using(function (array $data, Command $record): void {
|
||||
app(EditCommand::class)->edit($record, $data);
|
||||
$this->dispatch('$refresh');
|
||||
})
|
||||
->modalWidth(MaxWidth::Medium),
|
||||
DeleteAction::make('delete')
|
||||
->icon('heroicon-o-trash')
|
||||
->hiddenLabel()
|
||||
->tooltip('Delete')
|
||||
->modalHeading('Delete Command')
|
||||
->authorize(fn (Command $record) => $user->can('delete', [$record, $this->site, $this->site->server]))
|
||||
->using(function (array $data, Command $record): void {
|
||||
$record->delete();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Widgets;
|
||||
|
||||
use App\Models\Deployment;
|
||||
use App\Models\Site;
|
||||
use App\Models\User;
|
||||
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;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
class DeploymentsList extends Widget
|
||||
{
|
||||
public Site $site;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
/**
|
||||
* @return Builder<Deployment>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Deployment::query()->where('site_id', $this->site->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<Deployment> $query
|
||||
* @return Builder<Deployment>
|
||||
*/
|
||||
protected function applySortingToTableQuery(Builder $query): Builder
|
||||
{
|
||||
return $query->latest('created_at');
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('commit_data')
|
||||
->label('Commit')
|
||||
->url(fn (Deployment $record) => $record->commit_data['url'] ?? '#')
|
||||
->openUrlInNewTab()
|
||||
->formatStateUsing(fn (Deployment $record) => $record->commit_data['message'] ?? 'No message')
|
||||
->tooltip(fn (Deployment $record) => $record->commit_data['message'] ?? 'No message')
|
||||
->limit(50)
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->formatStateUsing(fn (Deployment $record) => $record->created_at_by_timezone)
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (Deployment $record) => Deployment::$statusColors[$record->status])
|
||||
->searchable()
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $table
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->heading('Deployments')
|
||||
->actions([
|
||||
Action::make('view')
|
||||
->hiddenLabel()
|
||||
->tooltip('View')
|
||||
->icon('heroicon-o-eye')
|
||||
->authorize(fn ($record) => $user->can('view', $record->log))
|
||||
->modalHeading('View Log')
|
||||
->modalContent(fn (Deployment $record) => view('components.console-view', [
|
||||
'slot' => $record->log?->getContent(),
|
||||
'attributes' => new ComponentAttributeBag,
|
||||
]))
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelActionLabel('Close'),
|
||||
Action::make('download')
|
||||
->hiddenLabel()
|
||||
->tooltip('Download')
|
||||
->color('gray')
|
||||
->icon('heroicon-o-archive-box-arrow-down')
|
||||
->authorize(fn ($record) => $user->can('view', $record->log))
|
||||
->action(fn (Deployment $record) => $record->log?->download()),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static string $view = '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());
|
||||
}
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Widgets;
|
||||
|
||||
use App\Actions\Site\UpdateLoadBalancer;
|
||||
use App\Enums\LoadBalancerMethod;
|
||||
use App\Models\LoadBalancerServer;
|
||||
use App\Models\Server;
|
||||
use App\Models\Site;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Widgets\Widget;
|
||||
use Livewire\Attributes\On;
|
||||
|
||||
class LoadBalancerServers extends Widget implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static string $view = 'components.form';
|
||||
|
||||
public Site $site;
|
||||
|
||||
public string $method;
|
||||
|
||||
/**
|
||||
* @var array<int, mixed>
|
||||
*/
|
||||
public array $servers = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->setLoadBalancerServers();
|
||||
if ($this->servers === []) {
|
||||
$this->servers = [
|
||||
[
|
||||
'server' => null,
|
||||
'port' => 80,
|
||||
'weight' => null,
|
||||
'backup' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
$this->method = $this->site->type_data['method'] ?? LoadBalancerMethod::ROUND_ROBIN;
|
||||
}
|
||||
|
||||
#[On('load-balancer-updated')]
|
||||
public function setLoadBalancerServers(): void
|
||||
{
|
||||
$this->servers = $this->site->loadBalancerServers->map(fn (LoadBalancerServer $server): array => [
|
||||
'server' => $server->ip,
|
||||
'port' => $server->port,
|
||||
'weight' => $server->weight,
|
||||
'backup' => $server->backup,
|
||||
])->toArray();
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
->heading('Load Balancer Servers')
|
||||
->description('You can add or remove servers from the load balancer here')
|
||||
->columns(3)
|
||||
->schema([
|
||||
Select::make('method')
|
||||
->label('Balancing Method')
|
||||
->validationAttribute('Balancing Method')
|
||||
->options(
|
||||
collect(LoadBalancerMethod::all())
|
||||
->mapWithKeys(fn ($method) => [$method => $method])
|
||||
)
|
||||
->rules(UpdateLoadBalancer::rules($this->site)['method']),
|
||||
Repeater::make('servers')
|
||||
->schema([
|
||||
Select::make('server')
|
||||
->placeholder('Select a server')
|
||||
->searchable()
|
||||
->required()
|
||||
->rules(UpdateLoadBalancer::rules($this->site)['servers.*.server'])
|
||||
->options(fn () => $this->site->project->servers()
|
||||
->where('id', '!=', $this->site->server_id)
|
||||
->whereNotNull('local_ip')
|
||||
->get()
|
||||
->mapWithKeys(function ($server): array {
|
||||
/** @var Server $server */
|
||||
return $server->local_ip ? [$server->local_ip => $server->name.' ('.$server->local_ip.')'] : [];
|
||||
})),
|
||||
TextInput::make('port')
|
||||
->default(80)
|
||||
->required()
|
||||
->rules(UpdateLoadBalancer::rules($this->site)['servers.*.port']),
|
||||
TextInput::make('weight')
|
||||
->rules(UpdateLoadBalancer::rules($this->site)['servers.*.weight']),
|
||||
Toggle::make('backup')
|
||||
->label('Backup')
|
||||
->inline(false)
|
||||
->default(false),
|
||||
])
|
||||
->columnSpan(3)
|
||||
->live()
|
||||
->reorderable(false)
|
||||
->columns(4)
|
||||
->reorderableWithDragAndDrop(false)
|
||||
->addActionLabel('Add Server'),
|
||||
])
|
||||
->footerActions([
|
||||
Action::make('save')
|
||||
->label('Save')
|
||||
->action(fn () => $this->save()),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->authorize('update', [$this->site, $this->site->server]);
|
||||
|
||||
$this->validate();
|
||||
|
||||
run_action($this, function (): void {
|
||||
app(UpdateLoadBalancer::class)->update($this->site, [
|
||||
'method' => $this->method,
|
||||
'servers' => $this->servers,
|
||||
]);
|
||||
|
||||
$this->dispatch('load-balancer-updated');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Load balancer updated!')
|
||||
->send();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,204 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Widgets;
|
||||
|
||||
use App\Actions\Site\UpdateAliases;
|
||||
use App\Actions\Site\UpdatePHPVersion;
|
||||
use App\Actions\Site\UpdateSourceControl;
|
||||
use App\Models\Site;
|
||||
use App\Models\SourceControl;
|
||||
use App\Models\User;
|
||||
use App\Web\Pages\Settings\SourceControls\Actions\Create;
|
||||
use App\Web\Pages\Settings\Tags\Actions\EditTags;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Infolists\Components\Actions\Action;
|
||||
use Filament\Infolists\Components\Section;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Infolists\Concerns\InteractsWithInfolists;
|
||||
use Filament\Infolists\Contracts\HasInfolists;
|
||||
use Filament\Infolists\Infolist;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Widgets\Widget;
|
||||
|
||||
class SiteDetails extends Widget implements HasForms, HasInfolists
|
||||
{
|
||||
use InteractsWithForms;
|
||||
use InteractsWithInfolists;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static string $view = 'components.infolist';
|
||||
|
||||
public Site $site;
|
||||
|
||||
public function infolist(Infolist $infolist): Infolist
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $infolist
|
||||
->schema([
|
||||
Section::make()
|
||||
->heading('Site Details')
|
||||
->description('More details about your site')
|
||||
->columns(1)
|
||||
->schema([
|
||||
TextEntry::make('id')
|
||||
->label('ID')
|
||||
->inlineLabel()
|
||||
->hintIcon('heroicon-o-information-circle')
|
||||
->hintIconTooltip('Site unique identifier to use in the API'),
|
||||
TextEntry::make('user')
|
||||
->label('Site User')
|
||||
->inlineLabel(),
|
||||
TextEntry::make('created_at')
|
||||
->label('Created At')
|
||||
->formatStateUsing(fn ($record) => $record->created_at_by_timezone)
|
||||
->inlineLabel(),
|
||||
TextEntry::make('type')
|
||||
->extraAttributes(['class' => 'capitalize'])
|
||||
->icon(fn ($state): string => 'icon-'.$state)
|
||||
->inlineLabel(),
|
||||
TextEntry::make('tags.*')
|
||||
->default('No tags')
|
||||
->formatStateUsing(fn ($state) => is_object($state) && isset($state->name) ? $state->name : $state)
|
||||
->inlineLabel()
|
||||
->badge()
|
||||
->color(fn ($state) => is_object($state) && isset($state->color) ? $state->color : 'gray')
|
||||
->icon(fn ($state): string => is_object($state) ? 'heroicon-o-tag' : '')
|
||||
->suffixAction(
|
||||
EditTags::infolist($this->site)
|
||||
),
|
||||
TextEntry::make('php_version')
|
||||
->label('PHP Version')
|
||||
->inlineLabel()
|
||||
->suffixAction(
|
||||
Action::make('edit_php_version')
|
||||
->icon('heroicon-o-pencil-square')
|
||||
->tooltip('Change')
|
||||
->modalSubmitActionLabel('Save')
|
||||
->modalHeading('Update PHP Version')
|
||||
->modalWidth(MaxWidth::Medium)
|
||||
->form([
|
||||
Select::make('version')
|
||||
->label('Version')
|
||||
->selectablePlaceholder(false)
|
||||
->rules(UpdatePHPVersion::rules($this->site)['version'])
|
||||
->default($this->site->php_version)
|
||||
->options(
|
||||
collect($this->site->server->installedPHPVersions())
|
||||
->mapWithKeys(fn ($version) => [$version => $version])
|
||||
),
|
||||
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
app(UpdatePHPVersion::class)->update($this->site, $data);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('PHP version updated!')
|
||||
->send();
|
||||
});
|
||||
})
|
||||
),
|
||||
TextEntry::make('aliases.*')
|
||||
->inlineLabel()
|
||||
->badge()
|
||||
->default('No aliases')
|
||||
->color(fn ($state): string => $state == 'No aliases' ? 'gray' : 'primary')
|
||||
->suffixAction(
|
||||
Action::make('edit_aliases')
|
||||
->icon('heroicon-o-pencil-square')
|
||||
->tooltip('Change')
|
||||
->modalSubmitActionLabel('Save')
|
||||
->modalHeading('Update Aliases')
|
||||
->modalWidth(MaxWidth::Medium)
|
||||
->form([
|
||||
TagsInput::make('aliases')
|
||||
->splitKeys(['Enter', 'Tab', ' ', ','])
|
||||
->placeholder('Type and press enter to add an alias')
|
||||
->default($this->site->aliases)
|
||||
->nestedRecursiveRules(UpdateAliases::rules()['aliases.*']),
|
||||
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
app(UpdateAliases::class)->update($this->site, $data);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Aliases updated!')
|
||||
->send();
|
||||
});
|
||||
})
|
||||
),
|
||||
TextEntry::make('source_control_id')
|
||||
->label('Source Control')
|
||||
->visible(fn (Site $record) => $record->sourceControl)
|
||||
->formatStateUsing(fn (Site $record) => $record->sourceControl?->profile)
|
||||
->inlineLabel()
|
||||
->suffixAction(
|
||||
Action::make('edit_source_control')
|
||||
->icon('heroicon-o-pencil-square')
|
||||
->tooltip('Change')
|
||||
->modalSubmitActionLabel('Save')
|
||||
->modalHeading('Update Source Control')
|
||||
->modalWidth(MaxWidth::Medium)
|
||||
->form([
|
||||
Select::make('source_control')
|
||||
->label('Source Control')
|
||||
->rules(UpdateSourceControl::rules()['source_control'])
|
||||
->options(
|
||||
SourceControl::getByProjectId($user->current_project_id)
|
||||
->pluck('profile', 'id')
|
||||
)
|
||||
->default($this->site->source_control_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 () => $user->can('create', SourceControl::class))
|
||||
->action(fn (array $data) => Create::action($data))
|
||||
)
|
||||
->placeholder('Select source control'),
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
run_action($this, function () use ($data): void {
|
||||
app(UpdateSourceControl::class)->update($this->site, $data);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Source control updated!')
|
||||
->send();
|
||||
});
|
||||
})
|
||||
),
|
||||
TextEntry::make('repository')
|
||||
->label('Repository')
|
||||
->visible(fn (Site $record) => $record->repository)
|
||||
->formatStateUsing(fn (Site $record) => $record->repository)
|
||||
->inlineLabel(),
|
||||
TextEntry::make('branch')
|
||||
->label('Branch')
|
||||
->visible(fn (Site $record) => $record->branch)
|
||||
->formatStateUsing(fn (Site $record) => $record->branch)
|
||||
->inlineLabel(),
|
||||
]),
|
||||
])
|
||||
->record($this->site);
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static string $view = '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 fn ($state): string => Site::$statusColors[$state]),
|
||||
])
|
||||
->columns(3),
|
||||
])
|
||||
->record($this->site->refresh());
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\Site;
|
||||
use App\Models\User;
|
||||
use App\Web\Pages\Servers\Sites\Settings;
|
||||
use App\Web\Pages\Servers\Sites\View;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class SitesList extends Widget
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
/**
|
||||
* @return Builder<Site>
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Site::query()->where('server_id', $this->server->id);
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
IconColumn::make('type')
|
||||
->icon(fn (Site $record): string => 'icon-'.$record->type)
|
||||
->tooltip(fn (Site $record) => $record->type)
|
||||
->width(24),
|
||||
TextColumn::make('domain')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('tags')
|
||||
->label('Tags')
|
||||
->badge()
|
||||
->icon('heroicon-o-tag')
|
||||
->formatStateUsing(fn ($state) => $state->name)
|
||||
->color(fn ($state) => $state->color)
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->formatStateUsing(fn (Site $record) => $record->created_at_by_timezone)
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (Site $site) => Site::$statusColors[$site->status])
|
||||
->searchable()
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $table
|
||||
->heading(null)
|
||||
->query($this->getTableQuery())
|
||||
->columns($this->getTableColumns())
|
||||
->recordUrl(fn (Site $record): string => View::getUrl(parameters: ['server' => $this->server, 'site' => $record]))
|
||||
->actions([
|
||||
Action::make('settings')
|
||||
->label('Settings')
|
||||
->icon('heroicon-o-cog-6-tooth')
|
||||
->authorize(fn (Site $record) => $user->can('update', [$record, $this->server]))
|
||||
->url(fn (Site $record): string => Settings::getUrl(parameters: ['server' => $this->server, 'site' => $record])),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers;
|
||||
|
||||
use App\Models\ServerLog;
|
||||
use App\Models\User;
|
||||
use App\Web\Pages\Servers\Logs\Widgets\LogsList;
|
||||
use App\Web\Pages\Servers\Widgets\Installing;
|
||||
use App\Web\Pages\Servers\Widgets\ServerStats;
|
||||
use Livewire\Attributes\On;
|
||||
|
||||
class View extends Page
|
||||
{
|
||||
protected static ?string $slug = 'servers/{server}';
|
||||
|
||||
protected static ?string $title = 'Overview';
|
||||
|
||||
public string $previousStatus;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
$this->authorize('view', [$this->server, $user->currentProject]);
|
||||
$this->previousStatus = $this->server->status;
|
||||
}
|
||||
|
||||
#[On('$refresh')]
|
||||
public function refresh(): void
|
||||
{
|
||||
$currentStatus = $this->server->refresh()->status;
|
||||
|
||||
if ($this->previousStatus !== $currentStatus) {
|
||||
$this->redirect(static::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
$this->previousStatus = $currentStatus;
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
$widgets = [];
|
||||
|
||||
if ($this->server->isInstalling()) {
|
||||
$widgets[] = [Installing::class, ['server' => $this->server]];
|
||||
} else {
|
||||
$widgets[] = [ServerStats::class, ['server' => $this->server]];
|
||||
}
|
||||
|
||||
if ($user->can('viewAny', [ServerLog::class, $this->server])) {
|
||||
$widgets[] = [
|
||||
LogsList::class, [
|
||||
'server' => $this->server,
|
||||
'label' => 'Logs',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $widgets;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user