mirror of
https://github.com/vitodeploy/vito.git
synced 2025-04-19 09:51:37 +00:00
2.x
This commit is contained in:
parent
b62c40c97d
commit
f6bc04763b
@ -7,6 +7,7 @@ indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
ij_any_block_comment_at_first_column = false
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -21,3 +21,4 @@ yarn-error.log
|
||||
/.fleet
|
||||
/.idea
|
||||
/.vscode
|
||||
laradumps.yaml
|
||||
|
33
app/Actions/Projects/AddUser.php
Normal file
33
app/Actions/Projects/AddUser.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Projects;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AddUser
|
||||
{
|
||||
public function add(Project $project, array $input): void
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = User::query()->findOrFail($input['user']);
|
||||
|
||||
$project->users()->detach($user);
|
||||
$project->users()->attach($user);
|
||||
}
|
||||
|
||||
public static function rules(Project $project): array
|
||||
{
|
||||
return [
|
||||
'user' => [
|
||||
'required',
|
||||
Rule::exists('users', 'id'),
|
||||
Rule::unique('user_project', 'user_id')->where(function (Builder $query) use ($project) {
|
||||
$query->where('project_id', $project->id);
|
||||
}),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -27,15 +27,21 @@ public function create(User $user, array $input): Project
|
||||
return $project;
|
||||
}
|
||||
|
||||
private function validate(array $input): void
|
||||
public static function rules(): array
|
||||
{
|
||||
Validator::make($input, [
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
'unique:projects,name',
|
||||
'lowercase:projects,name',
|
||||
],
|
||||
])->validate();
|
||||
];
|
||||
}
|
||||
|
||||
private function validate(array $input): void
|
||||
{
|
||||
Validator::make($input, self::rules())->validate();
|
||||
}
|
||||
}
|
||||
|
@ -23,15 +23,21 @@ public function update(Project $project, array $input): Project
|
||||
return $project;
|
||||
}
|
||||
|
||||
private function validate(Project $project, array $input): void
|
||||
public static function rules(Project $project): array
|
||||
{
|
||||
Validator::make($input, [
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('projects')->where('user_id', $project->user_id)->ignore($project->id),
|
||||
Rule::unique('projects', 'name')->ignore($project->id),
|
||||
'lowercase:projects,name',
|
||||
],
|
||||
])->validate();
|
||||
];
|
||||
}
|
||||
|
||||
private function validate(Project $project, array $input): void
|
||||
{
|
||||
Validator::make($input, self::rules($project))->validate();
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@
|
||||
use App\Enums\FirewallRuleStatus;
|
||||
use App\Enums\ServerProvider;
|
||||
use App\Enums\ServerStatus;
|
||||
use App\Exceptions\ServerProviderError;
|
||||
use App\Facades\Notifier;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
@ -16,10 +15,8 @@
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
|
||||
class CreateServer
|
||||
@ -29,8 +26,6 @@ class CreateServer
|
||||
*/
|
||||
public function create(User $creator, array $input): Server
|
||||
{
|
||||
$this->validateInputs($input);
|
||||
|
||||
$server = new Server([
|
||||
'project_id' => $creator->currentProject->id,
|
||||
'user_id' => $creator->id,
|
||||
@ -56,12 +51,8 @@ public function create(User $creator, array $input): Server
|
||||
$server->provider_id = $input['server_provider'];
|
||||
}
|
||||
|
||||
// validate type
|
||||
$this->validateType($server, $input);
|
||||
$server->type_data = $server->type()->data($input);
|
||||
|
||||
// validate provider
|
||||
$this->validateProvider($server, $input);
|
||||
$server->provider_data = $server->provider()->data($input);
|
||||
|
||||
// save
|
||||
@ -85,11 +76,6 @@ public function create(User $creator, array $input): Server
|
||||
} catch (Exception $e) {
|
||||
$server->provider()->delete();
|
||||
DB::rollBack();
|
||||
if ($e instanceof ServerProviderError) {
|
||||
throw ValidationException::withMessages([
|
||||
'provider' => __('Provider Error: ').$e->getMessage(),
|
||||
]);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
@ -126,59 +112,82 @@ function () use ($server) {
|
||||
$bus->onConnection('ssh')->dispatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validateInputs(array $input): void
|
||||
public static function rules(array $input): array
|
||||
{
|
||||
$rules = [
|
||||
'provider' => 'required|in:'.implode(',', config('core.server_providers')),
|
||||
'name' => 'required',
|
||||
'os' => 'required|in:'.implode(',', config('core.operating_systems')),
|
||||
'provider' => [
|
||||
'required',
|
||||
Rule::in(config('core.server_providers')),
|
||||
],
|
||||
'name' => [
|
||||
'required',
|
||||
],
|
||||
'os' => [
|
||||
'required',
|
||||
Rule::in(config('core.operating_systems')),
|
||||
],
|
||||
'type' => [
|
||||
'required',
|
||||
Rule::in(config('core.server_types')),
|
||||
],
|
||||
'server_provider' => [
|
||||
Rule::when(function () use ($input) {
|
||||
return $input['provider'] != ServerProvider::CUSTOM;
|
||||
}, [
|
||||
'required',
|
||||
'exists:server_providers,id,user_id,'.auth()->user()->id,
|
||||
]),
|
||||
],
|
||||
'ip' => [
|
||||
Rule::when(function () use ($input) {
|
||||
return $input['provider'] == ServerProvider::CUSTOM;
|
||||
}, [
|
||||
'required',
|
||||
new RestrictedIPAddressesRule,
|
||||
]),
|
||||
],
|
||||
'port' => [
|
||||
Rule::when(function () use ($input) {
|
||||
return $input['provider'] == ServerProvider::CUSTOM;
|
||||
}, [
|
||||
'required',
|
||||
'numeric',
|
||||
'min:1',
|
||||
'max:65535',
|
||||
]),
|
||||
],
|
||||
];
|
||||
|
||||
Validator::make($input, $rules)->validate();
|
||||
|
||||
if ($input['provider'] != 'custom') {
|
||||
$rules['server_provider'] = 'required|exists:server_providers,id,user_id,'.auth()->user()->id;
|
||||
}
|
||||
|
||||
if ($input['provider'] == 'custom') {
|
||||
$rules['ip'] = [
|
||||
'required',
|
||||
new RestrictedIPAddressesRule(),
|
||||
];
|
||||
$rules['port'] = [
|
||||
'required',
|
||||
'numeric',
|
||||
'min:1',
|
||||
'max:65535',
|
||||
];
|
||||
}
|
||||
|
||||
Validator::make($input, $rules)->validate();
|
||||
return array_merge($rules, self::typeRules($input), self::providerRules($input));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validateType(Server $server, array $input): void
|
||||
private static function typeRules(array $input): array
|
||||
{
|
||||
Validator::make($input, $server->type()->createRules($input))
|
||||
->validate();
|
||||
if (! isset($input['type']) || ! in_array($input['type'], config('core.server_types'))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$server = new Server(['type' => $input['type']]);
|
||||
|
||||
return $server->type()->createRules($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validateProvider(Server $server, array $input): void
|
||||
private static function providerRules(array $input): array
|
||||
{
|
||||
Validator::make($input, $server->provider()->createRules($input))
|
||||
->validate();
|
||||
if (
|
||||
! isset($input['provider']) ||
|
||||
! in_array($input['provider'], config('core.server_providers')) ||
|
||||
$input['provider'] == ServerProvider::CUSTOM
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$server = new Server([
|
||||
'provider' => $input['provider'],
|
||||
'provider_id' => $input['server_provider'],
|
||||
]);
|
||||
|
||||
return $server->provider()->createRules($input);
|
||||
}
|
||||
|
||||
private function createFirewallRules(Server $server): void
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
use App\Models\Server;
|
||||
use App\ValidationRules\RestrictedIPAddressesRule;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class EditServer
|
||||
@ -14,8 +14,6 @@ class EditServer
|
||||
*/
|
||||
public function edit(Server $server, array $input): Server
|
||||
{
|
||||
$this->validate($input);
|
||||
|
||||
$checkConnection = false;
|
||||
if (isset($input['name'])) {
|
||||
$server->name = $input['name'];
|
||||
@ -41,15 +39,24 @@ public function edit(Server $server, array $input): Server
|
||||
return $server;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
protected function validate(array $input): void
|
||||
public static function rules(Server $server): array
|
||||
{
|
||||
Validator::make($input, [
|
||||
'ip' => [
|
||||
new RestrictedIPAddressesRule(),
|
||||
return [
|
||||
'name' => [
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('servers')->where('project_id', $server->project_id)->ignore($server->id),
|
||||
],
|
||||
])->validateWithBag('editServer');
|
||||
'ip' => [
|
||||
'string',
|
||||
new RestrictedIPAddressesRule,
|
||||
Rule::unique('servers')->where('project_id', $server->project_id)->ignore($server->id),
|
||||
],
|
||||
'port' => [
|
||||
'integer',
|
||||
'min:1',
|
||||
'max:65535',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@
|
||||
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;
|
||||
|
||||
@ -17,11 +16,7 @@ class CreateServerProvider
|
||||
*/
|
||||
public function create(User $user, array $input): ServerProvider
|
||||
{
|
||||
$this->validateInput($input);
|
||||
|
||||
$provider = $this->getProvider($input['provider']);
|
||||
|
||||
$this->validateProvider($provider, $input);
|
||||
$provider = static::getProvider($input['provider']);
|
||||
|
||||
try {
|
||||
$provider->connect($input);
|
||||
@ -33,7 +28,7 @@ public function create(User $user, array $input): ServerProvider
|
||||
]);
|
||||
}
|
||||
|
||||
$serverProvider = new ServerProvider();
|
||||
$serverProvider = new ServerProvider;
|
||||
$serverProvider->user_id = $user->id;
|
||||
$serverProvider->profile = $input['name'];
|
||||
$serverProvider->provider = $input['provider'];
|
||||
@ -44,19 +39,16 @@ public function create(User $user, array $input): ServerProvider
|
||||
return $serverProvider;
|
||||
}
|
||||
|
||||
private function getProvider($name): ServerProviderContract
|
||||
private static function getProvider($name): ServerProviderContract
|
||||
{
|
||||
$providerClass = config('core.server_providers_class.'.$name);
|
||||
|
||||
return new $providerClass();
|
||||
return new $providerClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validateInput(array $input): void
|
||||
public static function rules(): array
|
||||
{
|
||||
Validator::make($input, [
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
],
|
||||
@ -65,14 +57,11 @@ private function validateInput(array $input): void
|
||||
Rule::in(config('core.server_providers')),
|
||||
Rule::notIn('custom'),
|
||||
],
|
||||
])->validate();
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validateProvider(ServerProviderContract $provider, array $input): void
|
||||
public static function providerRules(array $input): array
|
||||
{
|
||||
Validator::make($input, $provider->credentialValidationRules($input))->validate();
|
||||
return static::getProvider($input['provider'])->credentialValidationRules($input);
|
||||
}
|
||||
}
|
||||
|
@ -4,31 +4,23 @@
|
||||
|
||||
use App\Models\ServerProvider;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class EditServerProvider
|
||||
{
|
||||
public function edit(ServerProvider $serverProvider, User $user, array $input): void
|
||||
{
|
||||
$this->validate($input);
|
||||
|
||||
$serverProvider->profile = $input['name'];
|
||||
$serverProvider->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
|
||||
|
||||
$serverProvider->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validate(array $input): void
|
||||
public static function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
],
|
||||
];
|
||||
Validator::make($input, $rules)->validate();
|
||||
}
|
||||
}
|
||||
|
@ -101,22 +101,26 @@ public function create(Server $server, array $input): Site
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validateInputs(Server $server, array $input): void
|
||||
public static function rules(array $input): void
|
||||
{
|
||||
$rules = [
|
||||
'server_id' => [
|
||||
'required',
|
||||
'exists:servers,id',
|
||||
],
|
||||
'type' => [
|
||||
'required',
|
||||
Rule::in(config('core.site_types')),
|
||||
],
|
||||
'domain' => [
|
||||
'required',
|
||||
new DomainRule(),
|
||||
new DomainRule,
|
||||
Rule::unique('sites', 'domain')->where(function ($query) use ($server) {
|
||||
return $query->where('server_id', $server->id);
|
||||
}),
|
||||
],
|
||||
'aliases.*' => [
|
||||
new DomainRule(),
|
||||
new DomainRule,
|
||||
],
|
||||
];
|
||||
|
||||
|
@ -27,7 +27,12 @@ public function create(array $input): User
|
||||
|
||||
private function validate(array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
Validator::make($input, self::rules())->validate();
|
||||
}
|
||||
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'password' => 'required|string|min:8',
|
||||
@ -35,6 +40,6 @@ private function validate(array $input): void
|
||||
'required',
|
||||
Rule::in([UserRole::ADMIN, UserRole::USER]),
|
||||
],
|
||||
])->validate();
|
||||
];
|
||||
}
|
||||
}
|
||||
|
43
app/Actions/User/UpdateProjects.php
Normal file
43
app/Actions/User/UpdateProjects.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\User;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateProjects
|
||||
{
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
$this->validate($input);
|
||||
$user->projects()->sync($input['projects']);
|
||||
|
||||
if ($user->currentProject && !$user->projects->contains($user->currentProject)) {
|
||||
$user->current_project_id = null;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
/** @var Project $firstProject */
|
||||
$firstProject = $user->projects->first();
|
||||
if (!$user->currentProject && $firstProject) {
|
||||
$user->current_project_id = $firstProject->id;
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function validate(array $input): void
|
||||
{
|
||||
validator($input, self::rules())->validate();
|
||||
}
|
||||
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'projects.*' => [
|
||||
'required',
|
||||
Rule::exists('projects', 'id'),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@
|
||||
|
||||
class UpdateUser
|
||||
{
|
||||
public function update(User $user, array $input): void
|
||||
public function update(User $user, array $input): User
|
||||
{
|
||||
$this->validate($user, $input);
|
||||
|
||||
@ -18,18 +18,29 @@ public function update(User $user, array $input): void
|
||||
$user->timezone = $input['timezone'];
|
||||
$user->role = $input['role'];
|
||||
|
||||
if (isset($input['password']) && $input['password'] !== null) {
|
||||
if (isset($input['password'])) {
|
||||
$user->password = bcrypt($input['password']);
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function validate(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
Validator::make($input, self::rules($user))->validate();
|
||||
}
|
||||
|
||||
public static function rules(User $user): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||
'email' => [
|
||||
'required',
|
||||
'email', 'max:255',
|
||||
Rule::unique('users', 'email')->ignore($user->id)
|
||||
],
|
||||
'timezone' => [
|
||||
'required',
|
||||
Rule::in(timezone_identifiers_list()),
|
||||
@ -37,12 +48,7 @@ private function validate(User $user, array $input): void
|
||||
'role' => [
|
||||
'required',
|
||||
Rule::in([UserRole::ADMIN, UserRole::USER]),
|
||||
function ($attribute, $value, $fail) use ($user) {
|
||||
if ($user->is(auth()->user()) && $value !== $user->role) {
|
||||
$fail('You cannot change your own role');
|
||||
}
|
||||
},
|
||||
],
|
||||
])->validate();
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -3,23 +3,22 @@
|
||||
namespace App\Actions\User;
|
||||
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class UpdateUserPassword
|
||||
{
|
||||
/**
|
||||
* Validate and update the user's password.
|
||||
*/
|
||||
public function update($user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'current_password' => ['required', 'string', 'current-password'],
|
||||
'password' => ['required', 'string'],
|
||||
'password_confirmation' => ['required', 'same:password'],
|
||||
])->validate();
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
}
|
||||
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'current_password' => ['required', 'string', 'current-password'],
|
||||
'password' => ['required', 'string'],
|
||||
'password_confirmation' => ['required', 'same:password'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -3,28 +3,12 @@
|
||||
namespace App\Actions\User;
|
||||
|
||||
use App\Models\User;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateUserProfileInformation
|
||||
{
|
||||
/**
|
||||
* Validate and update the given user's profile information.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||
'timezone' => [
|
||||
'required',
|
||||
Rule::in(timezone_identifiers_list()),
|
||||
],
|
||||
])->validateWithBag('updateProfileInformation');
|
||||
|
||||
if ($input['email'] !== $user->email) {
|
||||
$this->updateVerifiedUser($user, $input);
|
||||
} else {
|
||||
@ -36,6 +20,18 @@ public function update(User $user, array $input): void
|
||||
}
|
||||
}
|
||||
|
||||
public static function rules(User $user): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||
'timezone' => [
|
||||
'required',
|
||||
Rule::in(timezone_identifiers_list()),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given verified user's profile information.
|
||||
*/
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Actions\User\CreateUser;
|
||||
use App\Actions\User\UpdateProjects;
|
||||
use App\Actions\User\UpdateUser;
|
||||
use App\Facades\Toast;
|
||||
use App\Helpers\HtmxResponse;
|
||||
@ -48,26 +49,7 @@ public function update(User $user, Request $request): RedirectResponse
|
||||
|
||||
public function updateProjects(User $user, Request $request): HtmxResponse
|
||||
{
|
||||
$this->validate($request, [
|
||||
'projects.*' => [
|
||||
'required',
|
||||
Rule::exists('projects', 'id'),
|
||||
],
|
||||
]);
|
||||
|
||||
$user->projects()->sync($request->projects);
|
||||
|
||||
if ($user->currentProject && ! $user->projects->contains($user->currentProject)) {
|
||||
$user->current_project_id = null;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
/** @var Project $firstProject */
|
||||
$firstProject = $user->projects->first();
|
||||
if (! $user->currentProject && $firstProject) {
|
||||
$user->current_project_id = $firstProject->id;
|
||||
$user->save();
|
||||
}
|
||||
app(UpdateProjects::class)->update($user, $request->input());
|
||||
|
||||
Toast::success('Projects updated successfully');
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTimezoneTimestamps;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
@ -12,6 +13,8 @@
|
||||
*/
|
||||
abstract class AbstractModel extends Model
|
||||
{
|
||||
use HasTimezoneTimestamps;
|
||||
|
||||
public function jsonUpdate(string $field, string $key, mixed $value, bool $save = true): void
|
||||
{
|
||||
$current = $this->{$field};
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTimezoneTimestamps;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@ -18,12 +19,14 @@
|
||||
* @property Carbon $updated_at
|
||||
* @property User $user
|
||||
* @property Collection<Server> $servers
|
||||
* @property Collection<User> $users
|
||||
* @property Collection<NotificationChannel> $notificationChannels
|
||||
* @property Collection<SourceControl> $sourceControls
|
||||
*/
|
||||
class Project extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasTimezoneTimestamps;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
|
@ -3,12 +3,14 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Actions\Server\CheckConnection;
|
||||
use App\Enums\ServerStatus;
|
||||
use App\Enums\ServiceStatus;
|
||||
use App\Facades\SSH;
|
||||
use App\ServerTypes\ServerType;
|
||||
use App\SSH\Cron\Cron;
|
||||
use App\SSH\OS\OS;
|
||||
use App\SSH\Systemd\Systemd;
|
||||
use App\Support\Testing\SSHFake;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@ -56,6 +58,7 @@
|
||||
* @property Backup[] $backups
|
||||
* @property Queue[] $daemons
|
||||
* @property SshKey[] $sshKeys
|
||||
* @property Tag[] $tags
|
||||
* @property string $hostname
|
||||
* @property int $updates
|
||||
* @property Carbon $last_update_check
|
||||
@ -138,6 +141,29 @@ public static function boot(): void
|
||||
});
|
||||
}
|
||||
|
||||
public static array $statusColors = [
|
||||
ServerStatus::READY => 'success',
|
||||
ServerStatus::INSTALLING => 'warning',
|
||||
ServerStatus::DISCONNECTED => 'gray',
|
||||
ServerStatus::INSTALLATION_FAILED => 'danger',
|
||||
ServerStatus::UPDATING => 'warning',
|
||||
];
|
||||
|
||||
public function isReady(): bool
|
||||
{
|
||||
return $this->status === ServerStatus::READY;
|
||||
}
|
||||
|
||||
public function isInstalling(): bool
|
||||
{
|
||||
return in_array($this->status, [ServerStatus::INSTALLING, ServerStatus::INSTALLATION_FAILED]);
|
||||
}
|
||||
|
||||
public function isInstallationFailed(): bool
|
||||
{
|
||||
return $this->status === ServerStatus::INSTALLATION_FAILED;
|
||||
}
|
||||
|
||||
public function project(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Project::class, 'project_id');
|
||||
@ -268,7 +294,7 @@ public function defaultService($type): ?Service
|
||||
return $service;
|
||||
}
|
||||
|
||||
public function ssh(?string $user = null): mixed
|
||||
public function ssh(?string $user = null): \App\Helpers\SSH|SSHFake
|
||||
{
|
||||
return SSH::init($this, $user);
|
||||
}
|
||||
@ -295,7 +321,7 @@ public function provider(): \App\ServerProviders\ServerProvider
|
||||
{
|
||||
$providerClass = config('core.server_providers_class')[$this->provider];
|
||||
|
||||
return new $providerClass($this);
|
||||
return new $providerClass($this->serverProvider, $this);
|
||||
}
|
||||
|
||||
public function webserver(?string $version = null): ?Service
|
||||
@ -404,4 +430,13 @@ public function checkForUpdates(): void
|
||||
$this->last_update_check = now();
|
||||
$this->save();
|
||||
}
|
||||
|
||||
public function getAvailableUpdatesAttribute(?int $value): int
|
||||
{
|
||||
if (! $value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* @property int $user_id
|
||||
@ -15,6 +16,9 @@
|
||||
* @property bool $connected
|
||||
* @property User $user
|
||||
* @property ?int $project_id
|
||||
* @property Server[] $servers
|
||||
* @property Project $project
|
||||
* @property string $image_url
|
||||
*/
|
||||
class ServerProvider extends AbstractModel
|
||||
{
|
||||
@ -51,6 +55,13 @@ public function servers(): HasMany
|
||||
return $this->hasMany(Server::class, 'provider_id');
|
||||
}
|
||||
|
||||
public function provider(): \App\ServerProviders\ServerProvider
|
||||
{
|
||||
$providerClass = config('core.server_providers_class')[$this->provider];
|
||||
|
||||
return new $providerClass($this);
|
||||
}
|
||||
|
||||
public function project(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
@ -59,7 +70,54 @@ public function project(): BelongsTo
|
||||
public static function getByProjectId(int $projectId): Builder
|
||||
{
|
||||
return self::query()
|
||||
->where('project_id', $projectId)
|
||||
->orWhereNull('project_id');
|
||||
->where(function (Builder $query) use ($projectId) {
|
||||
$query->where('project_id', $projectId)
|
||||
->orWhereNull('project_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function getImageUrlAttribute(): string
|
||||
{
|
||||
return url('/static/images/'.$this->provider.'.svg');
|
||||
}
|
||||
|
||||
public static function regions(?int $id): array
|
||||
{
|
||||
if (! $id) {
|
||||
return [];
|
||||
}
|
||||
$profile = self::find($id);
|
||||
if (! $profile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Cache::get('regions-'.$id)) {
|
||||
return Cache::get('regions-'.$id);
|
||||
}
|
||||
|
||||
$regions = $profile->provider()->regions();
|
||||
Cache::put('regions-'.$id, $regions, 600);
|
||||
|
||||
return $regions;
|
||||
}
|
||||
|
||||
public static function plans(?int $id, ?string $region): array
|
||||
{
|
||||
if (! $id) {
|
||||
return [];
|
||||
}
|
||||
$profile = self::find($id);
|
||||
if (! $profile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Cache::get('plans-'.$id.'-'.$region)) {
|
||||
return Cache::get('plans-'.$id.'-'.$region);
|
||||
}
|
||||
|
||||
$plans = $profile->provider()->plans($region);
|
||||
Cache::put('plans-'.$id.'-'.$region, $plans, 600);
|
||||
|
||||
return $plans;
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,12 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\SiteStatus;
|
||||
use App\Exceptions\SourceControlIsNotConnected;
|
||||
use App\Exceptions\SSHError;
|
||||
use App\SiteTypes\SiteType;
|
||||
use App\SSH\Services\Webserver\Webserver;
|
||||
use App\Traits\HasProjectThroughServer;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@ -44,6 +46,7 @@
|
||||
class Site extends AbstractModel
|
||||
{
|
||||
use HasFactory;
|
||||
use HasProjectThroughServer;
|
||||
|
||||
protected $fillable = [
|
||||
'server_id',
|
||||
@ -73,6 +76,13 @@ class Site extends AbstractModel
|
||||
'source_control_id' => 'integer',
|
||||
];
|
||||
|
||||
public static array $statusColors = [
|
||||
SiteStatus::READY => 'success',
|
||||
SiteStatus::INSTALLING => 'warning',
|
||||
SiteStatus::INSTALLATION_FAILED => 'danger',
|
||||
SiteStatus::DELETING => 'danger',
|
||||
];
|
||||
|
||||
public static function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
@ -3,8 +3,13 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Traits\HasTimezoneTimestamps;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Models\Contracts\HasTenants;
|
||||
use Filament\Panel;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
@ -34,10 +39,13 @@
|
||||
* @property Project $currentProject
|
||||
* @property Collection<Project> $projects
|
||||
* @property string $role
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
*/
|
||||
class User extends Authenticatable
|
||||
class User extends Authenticatable implements HasTenants
|
||||
{
|
||||
use HasFactory;
|
||||
use HasTimezoneTimestamps;
|
||||
use Notifiable;
|
||||
use TwoFactorAuthenticatable;
|
||||
|
||||
@ -111,6 +119,16 @@ public function projects(): BelongsToMany
|
||||
return $this->belongsToMany(Project::class, 'user_project')->withTimestamps();
|
||||
}
|
||||
|
||||
public function getTenants(Panel $panel): Collection
|
||||
{
|
||||
return $this->projects;
|
||||
}
|
||||
|
||||
public function canAccessTenant(Model $tenant): bool
|
||||
{
|
||||
return $this->projects()->whereKey($tenant)->exists();
|
||||
}
|
||||
|
||||
public function currentProject(): HasOne
|
||||
{
|
||||
return $this->HasOne(Project::class, 'id', 'current_project_id');
|
||||
@ -121,7 +139,7 @@ public function createDefaultProject(): Project
|
||||
$project = $this->projects()->first();
|
||||
|
||||
if (! $project) {
|
||||
$project = new Project();
|
||||
$project = new Project;
|
||||
$project->name = 'default';
|
||||
$project->save();
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
|
||||
@ -10,26 +9,26 @@ class ProjectPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->role === UserRole::ADMIN;
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function view(User $user, Project $project): bool
|
||||
{
|
||||
return $user->role === UserRole::ADMIN || $project->users->contains($user);
|
||||
return $user->isAdmin() || $project->users->contains($user);
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->role === UserRole::ADMIN;
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function update(User $user, Project $project): bool
|
||||
{
|
||||
return $user->role === UserRole::ADMIN;
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function delete(User $user, Project $project): bool
|
||||
{
|
||||
return $user->role === UserRole::ADMIN;
|
||||
return $user->isAdmin();
|
||||
}
|
||||
}
|
||||
|
@ -2,40 +2,38 @@
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
|
||||
class ServerPolicy
|
||||
{
|
||||
public function viewAny(User $user, Project $project): bool
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->role === UserRole::ADMIN || $project->users->contains($user);
|
||||
return $user->isAdmin() || $user->currentProject?->users->contains($user);
|
||||
}
|
||||
|
||||
public function view(User $user, Server $server): bool
|
||||
{
|
||||
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
|
||||
return $user->isAdmin() || $server->project->users->contains($user);
|
||||
}
|
||||
|
||||
public function create(User $user, Project $project): bool
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->role === UserRole::ADMIN || $project->users->contains($user);
|
||||
return $user->isAdmin() || $user->currentProject?->users->contains($user);
|
||||
}
|
||||
|
||||
public function update(User $user, Server $server): bool
|
||||
{
|
||||
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
|
||||
return $user->isAdmin() || $server->project->users->contains($user);
|
||||
}
|
||||
|
||||
public function delete(User $user, Server $server): bool
|
||||
{
|
||||
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
|
||||
return $user->isAdmin() || $server->project->users->contains($user);
|
||||
}
|
||||
|
||||
public function manage(User $user, Server $server): bool
|
||||
{
|
||||
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
|
||||
return $user->isAdmin() || $server->project->users->contains($user);
|
||||
}
|
||||
}
|
||||
|
37
app/Policies/ServerProviderPolicy.php
Normal file
37
app/Policies/ServerProviderPolicy.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\ServerProvider;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class ServerProviderPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function view(User $user, ServerProvider $serverProvider): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function update(User $user, ServerProvider $serverProvider): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function delete(User $user, ServerProvider $serverProvider): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
}
|
41
app/Policies/SitePolicy.php
Normal file
41
app/Policies/SitePolicy.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\Site;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class SitePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny(User $user, Server $server): bool
|
||||
{
|
||||
return ($user->isAdmin() || $server->project->users->contains($user)) && $server->isReady();
|
||||
}
|
||||
|
||||
public function view(User $user, Site $site): bool
|
||||
{
|
||||
return ($user->isAdmin() || $site->server->project->users->contains($user)) &&
|
||||
$site->server->isReady();
|
||||
}
|
||||
|
||||
public function create(User $user, Server $server): bool
|
||||
{
|
||||
return ($user->isAdmin() || $server->project->users->contains($user)) && $server->isReady();
|
||||
}
|
||||
|
||||
public function update(User $user, Site $site): bool
|
||||
{
|
||||
return ($user->isAdmin() || $site->server->project->users->contains($user)) &&
|
||||
$site->server->isReady();
|
||||
}
|
||||
|
||||
public function delete(User $user, Site $site): bool
|
||||
{
|
||||
return ($user->isAdmin() || $site->server->project->users->contains($user)) &&
|
||||
$site->server->isReady();
|
||||
}
|
||||
}
|
36
app/Policies/UserPolicy.php
Normal file
36
app/Policies/UserPolicy.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class UserPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function view(User $user, User $model): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function update(User $user, User $model): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function delete(User $user, User $model): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ class RouteServiceProvider extends ServiceProvider
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const HOME = '/servers';
|
||||
public const HOME = '/app';
|
||||
|
||||
/**
|
||||
* Define your route model bindings, pattern filters, and other route configuration.
|
||||
|
99
app/Providers/WebServiceProvider.php
Normal file
99
app/Providers/WebServiceProvider.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
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\Panel;
|
||||
use Filament\Support\Colors\Color;
|
||||
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\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)
|
||||
);
|
||||
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('app')
|
||||
->passwordReset()
|
||||
->colors([
|
||||
'primary' => Color::Indigo,
|
||||
])
|
||||
->viteTheme('resources/css/filament/app/theme.css')
|
||||
->brandLogo(fn () => view('web.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,
|
||||
])
|
||||
->globalSearchKeyBindings(['command+k', 'ctrl+k'])
|
||||
->globalSearchFieldKeyBindingSuffix();
|
||||
}
|
||||
}
|
@ -10,7 +10,6 @@
|
||||
use Exception;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Throwable;
|
||||
|
||||
class AWS extends AbstractProvider
|
||||
@ -21,12 +20,7 @@ class AWS extends AbstractProvider
|
||||
|
||||
public function createRules(array $input): array
|
||||
{
|
||||
$rules = [
|
||||
'os' => [
|
||||
'required',
|
||||
Rule::in(config('core.operating_systems')),
|
||||
],
|
||||
];
|
||||
$rules = [];
|
||||
// plans
|
||||
$plans = [];
|
||||
foreach (config('serverproviders.aws.plans') as $plan) {
|
||||
@ -82,14 +76,18 @@ public function connect(?array $credentials = null): bool
|
||||
}
|
||||
}
|
||||
|
||||
public function plans(): array
|
||||
public function plans(?string $region): array
|
||||
{
|
||||
return config('serverproviders.aws.plans');
|
||||
return collect(config('serverproviders.aws.plans'))
|
||||
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function regions(): array
|
||||
{
|
||||
return config('serverproviders.aws.regions');
|
||||
return collect(config('serverproviders.aws.regions'))
|
||||
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function create(): void
|
||||
|
@ -3,15 +3,19 @@
|
||||
namespace App\ServerProviders;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerProvider as Provider;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
abstract class AbstractProvider implements ServerProvider
|
||||
{
|
||||
protected ?Provider $serverProvider;
|
||||
|
||||
protected ?Server $server;
|
||||
|
||||
public function __construct(?Server $server = null)
|
||||
public function __construct(?Provider $serverProvider = null, ?Server $server = null)
|
||||
{
|
||||
$this->serverProvider = $serverProvider;
|
||||
$this->server = $server;
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ public function createRules(array $input): array
|
||||
'ip' => [
|
||||
'required',
|
||||
Rule::unique('servers', 'ip'),
|
||||
new RestrictedIPAddressesRule(),
|
||||
new RestrictedIPAddressesRule,
|
||||
],
|
||||
'port' => [
|
||||
'required',
|
||||
@ -46,7 +46,7 @@ public function connect(?array $credentials = null): bool
|
||||
return true;
|
||||
}
|
||||
|
||||
public function plans(): array
|
||||
public function plans(?string $region): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
@ -16,9 +16,7 @@ class DigitalOcean extends AbstractProvider
|
||||
|
||||
public function createRules(array $input): array
|
||||
{
|
||||
$rules = [
|
||||
'os' => 'required|in:'.implode(',', config('core.operating_systems')),
|
||||
];
|
||||
$rules = [];
|
||||
// plans
|
||||
$plans = [];
|
||||
foreach (config('serverproviders.digitalocean.plans') as $plan) {
|
||||
@ -70,14 +68,18 @@ public function connect(?array $credentials = null): bool
|
||||
return true;
|
||||
}
|
||||
|
||||
public function plans(): array
|
||||
public function plans(?string $region): array
|
||||
{
|
||||
return config('serverproviders.digitalocean.plans');
|
||||
return collect(config('serverproviders.digitalocean.plans'))
|
||||
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function regions(): array
|
||||
{
|
||||
return config('serverproviders.digitalocean.regions');
|
||||
return collect(config('serverproviders.digitalocean.regions'))
|
||||
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -6,6 +6,8 @@
|
||||
use App\Exceptions\ServerProviderError;
|
||||
use App\Facades\Notifier;
|
||||
use App\Notifications\FailedToDeleteServerFromProvider;
|
||||
use Exception;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
@ -16,7 +18,6 @@ class Hetzner extends AbstractProvider
|
||||
public function createRules(array $input): array
|
||||
{
|
||||
return [
|
||||
'os' => 'required|in:'.implode(',', config('core.operating_systems')),
|
||||
'plan' => 'required',
|
||||
'region' => 'required',
|
||||
];
|
||||
@ -46,6 +47,7 @@ public function data(array $input): array
|
||||
|
||||
/**
|
||||
* @throws CouldNotConnectToProvider
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function connect(?array $credentials = null): bool
|
||||
{
|
||||
@ -57,18 +59,53 @@ public function connect(?array $credentials = null): bool
|
||||
return true;
|
||||
}
|
||||
|
||||
public function plans(): array
|
||||
public function plans(?string $region): array
|
||||
{
|
||||
return config('serverproviders.hetzner.plans');
|
||||
try {
|
||||
$plans = Http::withToken($this->serverProvider->credentials['token'])
|
||||
->get($this->apiUrl.'/server_types', ['per_page' => 50])
|
||||
->json();
|
||||
|
||||
return collect($plans['server_types'])->filter(function ($type) use ($region) {
|
||||
return collect($type['prices'])->filter(function ($price) use ($region) {
|
||||
return $price['location'] === $region;
|
||||
});
|
||||
})
|
||||
->mapWithKeys(function ($value) {
|
||||
return [
|
||||
$value['name'] => __('server_providers.plan', [
|
||||
'name' => $value['name'],
|
||||
'cpu' => $value['cores'],
|
||||
'architecture' => $value['architecture'],
|
||||
'memory' => $value['memory'],
|
||||
'disk' => $value['disk'],
|
||||
]),
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
} catch (Exception) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function regions(): array
|
||||
{
|
||||
return config('serverproviders.hetzner.regions');
|
||||
try {
|
||||
$regions = Http::withToken($this->serverProvider->credentials['token'])
|
||||
->get($this->apiUrl.'/locations', ['per_page' => 50])
|
||||
->json();
|
||||
|
||||
return collect($regions['locations'])
|
||||
->mapWithKeys(fn ($value) => [$value['name'] => $value['city'].' - '.$value['country']])
|
||||
->toArray();
|
||||
} catch (Exception) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ServerProviderError
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function create(): void
|
||||
{
|
||||
@ -106,6 +143,9 @@ public function create(): void
|
||||
$this->server->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function isRunning(): bool
|
||||
{
|
||||
$status = Http::withToken($this->server->serverProvider->credentials['token'])
|
||||
@ -118,6 +158,9 @@ public function isRunning(): bool
|
||||
return $status->json()['server']['status'] == 'running';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function delete(): void
|
||||
{
|
||||
if (isset($this->server->provider_data['hetzner_id'])) {
|
||||
|
@ -15,9 +15,7 @@ class Linode extends AbstractProvider
|
||||
|
||||
public function createRules($input): array
|
||||
{
|
||||
$rules = [
|
||||
'os' => 'required|in:'.implode(',', config('core.operating_systems')),
|
||||
];
|
||||
$rules = [];
|
||||
// plans
|
||||
$plans = [];
|
||||
foreach (config('serverproviders.linode.plans') as $plan) {
|
||||
@ -69,14 +67,18 @@ public function connect(?array $credentials = null): bool
|
||||
return true;
|
||||
}
|
||||
|
||||
public function plans(): array
|
||||
public function plans(?string $region): array
|
||||
{
|
||||
return config('serverproviders.linode.plans');
|
||||
return collect(config('serverproviders.linode.plans'))
|
||||
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function regions(): array
|
||||
{
|
||||
return config('serverproviders.linode.regions');
|
||||
return collect(config('serverproviders.linode.regions'))
|
||||
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,7 +14,7 @@ public function data(array $input): array;
|
||||
|
||||
public function connect(?array $credentials = null): bool;
|
||||
|
||||
public function plans(): array;
|
||||
public function plans(?string $region): array;
|
||||
|
||||
public function regions(): array;
|
||||
|
||||
|
@ -17,9 +17,7 @@ class Vultr extends AbstractProvider
|
||||
|
||||
public function createRules($input): array
|
||||
{
|
||||
$rules = [
|
||||
'os' => 'required|in:'.implode(',', config('core.operating_systems')),
|
||||
];
|
||||
$rules = [];
|
||||
// plans
|
||||
$plans = [];
|
||||
foreach (config('serverproviders.vultr.plans') as $plan) {
|
||||
@ -71,14 +69,18 @@ public function connect(?array $credentials = null): bool
|
||||
return true;
|
||||
}
|
||||
|
||||
public function plans(): array
|
||||
public function plans(?string $region): array
|
||||
{
|
||||
return config('serverproviders.vultr.plans');
|
||||
return collect(config('serverproviders.vultr.plans'))
|
||||
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function regions(): array
|
||||
{
|
||||
return config('serverproviders.vultr.regions');
|
||||
return collect(config('serverproviders.vultr.regions'))
|
||||
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,7 +27,7 @@ function date_with_timezone($date, $timezone): string
|
||||
|
||||
function htmx(): HtmxResponse
|
||||
{
|
||||
return new HtmxResponse();
|
||||
return new HtmxResponse;
|
||||
}
|
||||
|
||||
function vito_version(): string
|
||||
@ -48,3 +48,10 @@ function convert_time_format($string): string
|
||||
|
||||
return preg_replace('/(\d+)h/', '$1 hours', $string);
|
||||
}
|
||||
|
||||
function get_public_key_content(): string
|
||||
{
|
||||
return str(file_get_contents(storage_path(config('core.ssh_public_key_name'))))
|
||||
->replace("\n", '')
|
||||
->toString();
|
||||
}
|
||||
|
22
app/Traits/HasProjectThroughServer.php
Normal file
22
app/Traits/HasProjectThroughServer.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
|
||||
|
||||
trait HasProjectThroughServer
|
||||
{
|
||||
public function project(): HasOneThrough
|
||||
{
|
||||
return $this->hasOneThrough(
|
||||
Project::class,
|
||||
Server::class,
|
||||
'id',
|
||||
'id',
|
||||
'server_id',
|
||||
'project_id'
|
||||
);
|
||||
}
|
||||
}
|
35
app/Traits/HasTimezoneTimestamps.php
Normal file
35
app/Traits/HasTimezoneTimestamps.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @property string $created_at_by_timezone
|
||||
* @property string $updated_at_by_timezone
|
||||
*/
|
||||
trait HasTimezoneTimestamps
|
||||
{
|
||||
public function getCreatedAtByTimezoneAttribute(): string
|
||||
{
|
||||
return $this->getDateTimeByTimezone($this->created_at);
|
||||
}
|
||||
|
||||
public function getUpdatedAtByTimezoneAttribute(): string
|
||||
{
|
||||
return $this->getDateTimeByTimezone($this->updated_at);
|
||||
}
|
||||
|
||||
public function getDateTimeByTimezone(?Carbon $value = null): ?string
|
||||
{
|
||||
if ($value && auth()->user() && auth()->user()->timezone) {
|
||||
try {
|
||||
return date_with_timezone($value, auth()->user()->timezone);
|
||||
} catch (Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
72
app/Web/Fields/AlertField.php
Normal file
72
app/Web/Fields/AlertField.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Fields;
|
||||
|
||||
use Filament\Forms\Components\Field;
|
||||
|
||||
class AlertField extends Field
|
||||
{
|
||||
protected string $view = 'web.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;
|
||||
}
|
||||
}
|
10
app/Web/Fields/ProviderField.php
Normal file
10
app/Web/Fields/ProviderField.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Fields;
|
||||
|
||||
use Filament\Forms\Components\Field;
|
||||
|
||||
class ProviderField extends Field
|
||||
{
|
||||
protected string $view = 'web.fields.provider';
|
||||
}
|
15
app/Web/Pages/Dashboard.php
Normal file
15
app/Web/Pages/Dashboard.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages;
|
||||
|
||||
use Filament\Widgets\AccountWidget;
|
||||
|
||||
class Dashboard extends \Filament\Pages\Dashboard
|
||||
{
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
AccountWidget::class,
|
||||
];
|
||||
}
|
||||
}
|
48
app/Web/Pages/Servers/Create.php
Normal file
48
app/Web/Pages/Servers/Create.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Web\Traits\PageHasWidgets;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class Create extends Page
|
||||
{
|
||||
use PageHasWidgets;
|
||||
|
||||
protected static ?string $slug = 'servers/create';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static ?string $title = 'Create Server';
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()?->can('create', Server::class) ?? false;
|
||||
}
|
||||
|
||||
protected function getExtraAttributes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\CreateServer::class],
|
||||
];
|
||||
}
|
||||
|
||||
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/create-server.html')
|
||||
->openUrlInNewTab(),
|
||||
];
|
||||
}
|
||||
}
|
43
app/Web/Pages/Servers/Index.php
Normal file
43
app/Web/Pages/Servers/Index.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Web\Traits\PageHasWidgets;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
use PageHasWidgets;
|
||||
|
||||
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 getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\ServersList::class],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('create')
|
||||
->label('Create a Server')
|
||||
->url(Create::getUrl())
|
||||
->authorize('create', Server::class),
|
||||
];
|
||||
}
|
||||
}
|
78
app/Web/Pages/Servers/Settings.php
Normal file
78
app/Web/Pages/Servers/Settings.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers;
|
||||
|
||||
use App\Actions\Server\RebootServer;
|
||||
use App\Models\Server;
|
||||
use App\Web\Pages\Servers\Widgets\ServerDetails;
|
||||
use App\Web\Pages\Servers\Widgets\UpdateServerInfo;
|
||||
use App\Web\Traits\PageHasServer;
|
||||
use App\Web\Traits\PageHasWidgets;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class Settings extends Page
|
||||
{
|
||||
use PageHasServer;
|
||||
use PageHasWidgets;
|
||||
|
||||
protected static ?string $slug = 'servers/{server}/settings';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static ?string $title = 'Settings';
|
||||
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
public Server $server;
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()?->can('update', request()->route('server')) ?? false;
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
ServerDetails::class, ['server' => $this->server],
|
||||
],
|
||||
[
|
||||
UpdateServerInfo::class, ['server' => $this->server],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make()
|
||||
->icon('heroicon-o-trash')
|
||||
->record($this->server)
|
||||
->modalHeading('Delete Server')
|
||||
->modalDescription('Once your server is deleted, all of its resources and data will be permanently deleted and can\'t be restored'),
|
||||
Action::make('reboot')
|
||||
->color('gray')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->label('Reboot')
|
||||
->requiresConfirmation()
|
||||
->action(function () {
|
||||
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;
|
||||
}
|
||||
}
|
42
app/Web/Pages/Servers/Sites/Index.php
Normal file
42
app/Web/Pages/Servers/Sites/Index.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\Site;
|
||||
use App\Web\Pages\Servers\Sites\Widgets\SitesList;
|
||||
use App\Web\Traits\PageHasServer;
|
||||
use App\Web\Traits\PageHasWidgets;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
use PageHasServer;
|
||||
use PageHasWidgets;
|
||||
|
||||
protected static ?string $slug = 'servers/{server}/sites';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static ?string $title = 'Sites';
|
||||
|
||||
public Server $server;
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[SitesList::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->authorize(fn () => auth()->user()?->can('create', [Site::class, $this->server]))
|
||||
->createAnother(false)
|
||||
->label('Create a Site'),
|
||||
];
|
||||
}
|
||||
}
|
57
app/Web/Pages/Servers/Sites/Widgets/SitesList.php
Normal file
57
app/Web/Pages/Servers/Sites/Widgets/SitesList.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\Site;
|
||||
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;
|
||||
|
||||
class SitesList extends Widget
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Site::query()->where('server_id', $this->server->id);
|
||||
}
|
||||
|
||||
protected static ?string $heading = '';
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('id')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('server.name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('domain')
|
||||
->searchable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (Site $site) => Site::$statusColors[$site->status])
|
||||
->searchable()
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getTable(): Table
|
||||
{
|
||||
return $this->table
|
||||
// ->recordUrl(fn (Server $record) => View::getUrl(parameters: ['server' => $record]))
|
||||
->actions([
|
||||
// Action::make('settings')
|
||||
// ->label('Settings')
|
||||
// ->icon('heroicon-o-cog-6-tooth')
|
||||
// ->authorize(fn ($record) => auth()->user()->can('update', $record))
|
||||
// ->url(fn (Server $record) => '/'),
|
||||
]);
|
||||
}
|
||||
}
|
59
app/Web/Pages/Servers/View.php
Normal file
59
app/Web/Pages/Servers/View.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Web\Pages\Servers\Widgets\Installing;
|
||||
use App\Web\Traits\PageHasServer;
|
||||
use App\Web\Traits\PageHasWidgets;
|
||||
use Filament\Pages\Page;
|
||||
use Livewire\Attributes\On;
|
||||
|
||||
class View extends Page
|
||||
{
|
||||
use PageHasServer;
|
||||
use PageHasWidgets;
|
||||
|
||||
protected static ?string $slug = 'servers/{server}';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static ?string $title = 'Overview';
|
||||
|
||||
public Server $server;
|
||||
|
||||
public string $previousStatus;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->previousStatus = $this->server->status;
|
||||
}
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()?->can('view', request()->route('server')) ?? false;
|
||||
}
|
||||
|
||||
#[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
|
||||
{
|
||||
if ($this->server->isInstalling()) {
|
||||
return [
|
||||
[Installing::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
263
app/Web/Pages/Servers/Widgets/CreateServer.php
Normal file
263
app/Web/Pages/Servers/Widgets/CreateServer.php
Normal file
@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Widgets;
|
||||
|
||||
use App\Actions\Server\CreateServer as CreateServerAction;
|
||||
use App\Enums\ServerProvider;
|
||||
use App\Enums\ServerType;
|
||||
use App\Enums\Webserver;
|
||||
use App\Models\Server;
|
||||
use App\Web\Fields\AlertField;
|
||||
use App\Web\Fields\ProviderField;
|
||||
use App\Web\Pages\Servers\Index;
|
||||
use App\Web\Pages\Settings\ServerProviders\Actions\Create;
|
||||
use Filament\Forms\Components\Actions;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Widgets\Widget;
|
||||
use Throwable;
|
||||
|
||||
class CreateServer extends Widget implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static string $view = 'web.components.form';
|
||||
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
public ?string $provider = ServerProvider::HETZNER;
|
||||
|
||||
public ?string $server_provider = '';
|
||||
|
||||
public ?string $region = '';
|
||||
|
||||
public ?string $plan = '';
|
||||
|
||||
public ?string $public_key = '';
|
||||
|
||||
public ?string $name = '';
|
||||
|
||||
public ?string $ip = '';
|
||||
|
||||
public ?string $port = '';
|
||||
|
||||
public ?string $os = '';
|
||||
|
||||
public ?string $type = ServerType::REGULAR;
|
||||
|
||||
public ?string $webserver = Webserver::NGINX;
|
||||
|
||||
public ?string $database = '';
|
||||
|
||||
public ?string $php = '';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
$publicKey = __('servers.create.public_key_text', [
|
||||
'public_key' => get_public_key_content(),
|
||||
]);
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
ProviderField::make('provider')
|
||||
->label('Select a provider')
|
||||
->default(ServerProvider::CUSTOM)
|
||||
->live()
|
||||
->reactive()
|
||||
->reactive()
|
||||
->afterStateUpdated(function (callable $set) {
|
||||
$set('server_provider', null);
|
||||
$set('region', null);
|
||||
$set('plan', null);
|
||||
})
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['provider']),
|
||||
AlertField::make('alert')
|
||||
->warning()
|
||||
->message(__('servers.create.public_key_warning'))
|
||||
->visible(fn ($get) => $this->provider === ServerProvider::CUSTOM),
|
||||
Select::make('server_provider')
|
||||
->visible(fn ($get) => $this->provider !== ServerProvider::CUSTOM)
|
||||
->label('Server provider connection')
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['server_provider'])
|
||||
->options(function ($get) {
|
||||
return \App\Models\ServerProvider::getByProjectId(auth()->user()->current_project_id)
|
||||
->where('provider', $this->provider)
|
||||
->pluck('profile', 'id');
|
||||
})
|
||||
->live()
|
||||
->suffixAction(
|
||||
Action::make('connect')
|
||||
->form(Create::form())
|
||||
->modalHeading('Connect to a new server provider')
|
||||
->modalSubmitActionLabel('Connect')
|
||||
->icon('heroicon-o-wifi')
|
||||
->tooltip('Connect to a new server provider')
|
||||
->modalWidth(MaxWidth::Medium)
|
||||
->authorize(fn () => auth()->user()->can('create', \App\Models\ServerProvider::class))
|
||||
// TODO: remove this after filament #14319 is fixed
|
||||
->url(\App\Web\Pages\Settings\ServerProviders\Index::getUrl())
|
||||
->action(fn (array $data) => Create::action($data))
|
||||
)
|
||||
->placeholder('Select profile')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->visible(fn ($get) => $this->provider !== ServerProvider::CUSTOM),
|
||||
Grid::make()
|
||||
->schema([
|
||||
Select::make('region')
|
||||
->label('Region')
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['region'])
|
||||
->live()
|
||||
->reactive()
|
||||
->options(function () {
|
||||
if (! $this->server_provider) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return \App\Models\ServerProvider::regions($this->server_provider);
|
||||
})
|
||||
->loadingMessage('Loading regions...')
|
||||
->disabled(fn ($get) => ! $this->server_provider)
|
||||
->placeholder(fn ($get) => $this->server_provider ? 'Select region' : 'Select connection first')
|
||||
->searchable(),
|
||||
Select::make('plan')
|
||||
->label('Plan')
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['plan'])
|
||||
->reactive()
|
||||
->options(function () {
|
||||
if (! $this->server_provider || ! $this->region) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return \App\Models\ServerProvider::plans($this->server_provider, $this->region);
|
||||
})
|
||||
->loadingMessage('Loading plans...')
|
||||
->disabled(fn ($get) => ! $this->region)
|
||||
->placeholder(fn ($get) => $this->region ? 'Select plan' : 'Select plan first')
|
||||
->searchable(),
|
||||
])
|
||||
->visible(fn ($get) => $this->provider !== ServerProvider::CUSTOM),
|
||||
TextInput::make('public_key')
|
||||
->label('Public Key')
|
||||
->default($publicKey)
|
||||
->suffixAction(
|
||||
Action::make('copy')
|
||||
->icon('heroicon-o-clipboard-document-list')
|
||||
->tooltip('Copy')
|
||||
->action(function ($livewire, $state) {
|
||||
$livewire->js(
|
||||
'window.navigator.clipboard.writeText("'.$state.'");'
|
||||
);
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Copied!')
|
||||
->send();
|
||||
})
|
||||
)
|
||||
->helperText('Run this command on your server as root user')
|
||||
->disabled()
|
||||
->visible(fn ($get) => $this->provider === ServerProvider::CUSTOM),
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['name']),
|
||||
Grid::make()
|
||||
->schema([
|
||||
TextInput::make('ip')
|
||||
->label('SSH IP Address')
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['ip']),
|
||||
TextInput::make('port')
|
||||
->label('SSH Port')
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['port']),
|
||||
])
|
||||
->visible(fn ($get) => $this->provider === ServerProvider::CUSTOM),
|
||||
Grid::make()
|
||||
->schema([
|
||||
Select::make('os')
|
||||
->label('OS')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['os'])
|
||||
->options(function () {
|
||||
return collect(config('core.operating_systems'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value]);
|
||||
}),
|
||||
Select::make('type')
|
||||
->label('Server Type')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['type'])
|
||||
->options(function () {
|
||||
return collect(config('core.server_types'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value]);
|
||||
})
|
||||
->default(ServerType::REGULAR),
|
||||
]),
|
||||
Grid::make(3)
|
||||
->schema([
|
||||
Select::make('webserver')
|
||||
->label('Webserver')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['webserver'] ?? [])
|
||||
->options(function () {
|
||||
return collect(config('core.webservers'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value]);
|
||||
}),
|
||||
Select::make('database')
|
||||
->label('Database')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['database'] ?? [])
|
||||
->options(function () {
|
||||
return collect(config('core.databases_name'))
|
||||
->mapWithKeys(fn ($value, $key) => [
|
||||
$key => $value.' '.config('core.databases_version')[$key],
|
||||
]);
|
||||
}),
|
||||
Select::make('php')
|
||||
->label('PHP')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['php'] ?? [])
|
||||
->options(function () {
|
||||
return collect(config('core.php_versions'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value]);
|
||||
}),
|
||||
]),
|
||||
Actions::make([
|
||||
Action::make('create')
|
||||
->label('Create Server')
|
||||
->button()
|
||||
->action(fn () => $this->submit()),
|
||||
]),
|
||||
])
|
||||
->columns(1);
|
||||
}
|
||||
|
||||
public function submit(): void
|
||||
{
|
||||
$this->authorize('create', Server::class);
|
||||
|
||||
$this->validate();
|
||||
|
||||
try {
|
||||
$server = app(CreateServerAction::class)->create(auth()->user(), $this->all()['data']);
|
||||
|
||||
$this->redirect(Index::getUrl());
|
||||
} catch (Throwable $e) {
|
||||
Notification::make()
|
||||
->title($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
57
app/Web/Pages/Servers/Widgets/Installing.php
Normal file
57
app/Web/Pages/Servers/Widgets/Installing.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Infolists\Components\Section;
|
||||
use Filament\Infolists\Components\ViewEntry;
|
||||
use Filament\Infolists\Concerns\InteractsWithInfolists;
|
||||
use Filament\Infolists\Contracts\HasInfolists;
|
||||
use Filament\Infolists\Infolist;
|
||||
use Filament\Widgets\Widget;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
class Installing extends Widget implements HasForms, HasInfolists
|
||||
{
|
||||
use InteractsWithForms;
|
||||
use InteractsWithInfolists;
|
||||
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static string $view = 'web.components.infolist';
|
||||
|
||||
public Server $server;
|
||||
|
||||
public function infolist(Infolist $infolist): Infolist
|
||||
{
|
||||
return $infolist
|
||||
->schema([
|
||||
Section::make()
|
||||
->heading('Installing Server')
|
||||
->icon(function () {
|
||||
if ($this->server->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->server->isInstallationFailed() ? 'danger' : 'primary')
|
||||
->schema([
|
||||
ViewEntry::make('progress')
|
||||
->hiddenLabel()
|
||||
->view('components.progress-bar')
|
||||
->viewData([
|
||||
'value' => $this->server->progress,
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->record($this->server->refresh());
|
||||
}
|
||||
}
|
101
app/Web/Pages/Servers/Widgets/SelectServer.php
Normal file
101
app/Web/Pages/Servers/Widgets/SelectServer.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Web\Resources\Server\Pages\CreateServer;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Widgets\Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
class SelectServer extends Widget implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static string $view = 'web.components.form';
|
||||
|
||||
public int|string|null $server = null;
|
||||
|
||||
/**
|
||||
* @throws ContainerExceptionInterface
|
||||
* @throws NotFoundExceptionInterface
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$server = Server::query()
|
||||
->where('project_id', auth()->user()->current_project_id)
|
||||
->find(session()->get('current_server_id'));
|
||||
if ($server && auth()->user()->can('view', $server)) {
|
||||
$this->server = $server->id;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
$options = $this->query()
|
||||
->limit(10)
|
||||
->pluck('name', 'id')
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
Select::make('server')
|
||||
->name('server')
|
||||
->model($this->server)
|
||||
->searchable()
|
||||
->options($options)
|
||||
->searchPrompt('Search...')
|
||||
->getSearchResultsUsing(function ($search) {
|
||||
return $this->query()
|
||||
->where('name', 'like', "%{$search}%")
|
||||
->limit(10)
|
||||
->pluck('name', 'id')
|
||||
->toArray();
|
||||
})
|
||||
->extraAttributes(['class' => '-mx-2 pointer-choices'])
|
||||
->live()
|
||||
->hintIcon('heroicon-o-question-mark-circle')
|
||||
->hintIconTooltip('Filter resources by default based on a server')
|
||||
->suffixAction(
|
||||
Action::make('create')
|
||||
->icon('heroicon-o-plus')
|
||||
->tooltip('Create a new server')
|
||||
->url(CreateServer::getUrl())
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private function query(): Builder
|
||||
{
|
||||
return Server::query()
|
||||
->where(function (Builder $query) {
|
||||
$query->where('project_id', auth()->user()->current_project_id);
|
||||
});
|
||||
}
|
||||
|
||||
public function updatedServer($value): void
|
||||
{
|
||||
if (! $value) {
|
||||
session()->forget('current_server_id');
|
||||
$this->redirect(url()->previous());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$server = Server::query()->find($value);
|
||||
if (! $server) {
|
||||
session()->forget('current_server_id');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->authorize('view', $server);
|
||||
session()->put('current_server_id', $value);
|
||||
|
||||
$this->redirect(url()->previous());
|
||||
}
|
||||
}
|
95
app/Web/Pages/Servers/Widgets/ServerDetails.php
Normal file
95
app/Web/Pages/Servers/Widgets/ServerDetails.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Widgets;
|
||||
|
||||
use App\Actions\Server\Update;
|
||||
use App\Models\Server;
|
||||
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\Widgets\Widget;
|
||||
|
||||
class ServerDetails extends Widget implements HasForms, HasInfolists
|
||||
{
|
||||
use InteractsWithForms;
|
||||
use InteractsWithInfolists;
|
||||
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static string $view = 'web.components.infolist';
|
||||
|
||||
public Server $server;
|
||||
|
||||
public function infolist(Infolist $infolist): Infolist
|
||||
{
|
||||
return $infolist
|
||||
->schema([
|
||||
Section::make()
|
||||
->heading('Server Details')
|
||||
->description('More details about your server')
|
||||
->columns(1)
|
||||
->schema([
|
||||
TextEntry::make('id')
|
||||
->label('ID')
|
||||
->inlineLabel()
|
||||
->hintIcon('heroicon-o-information-circle')
|
||||
->hintIconTooltip('Server unique identifier to use in the API'),
|
||||
TextEntry::make('created_at_by_timezone')
|
||||
->label('Created At')
|
||||
->inlineLabel(),
|
||||
TextEntry::make('last_updated_check')
|
||||
->label('Last Updated Check')
|
||||
->inlineLabel()
|
||||
->state(fn (Server $record) => $record->getDateTimeByTimezone($record->last_update_check) ?? '-')
|
||||
->suffixAction(
|
||||
Action::make('check-update')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->tooltip('Check Now')
|
||||
->action(fn (Server $record) => $record->checkForUpdates())
|
||||
),
|
||||
TextEntry::make('available_updates')
|
||||
->label('Available Updates')
|
||||
->inlineLabel()
|
||||
->suffixAction(
|
||||
Action::make('update-server')
|
||||
->icon('heroicon-o-check-circle')
|
||||
->tooltip('Update Now')
|
||||
->action(function (Server $record) {
|
||||
app(Update::class)->update($record);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->info()
|
||||
->title('Updating the server...')
|
||||
->send();
|
||||
})
|
||||
),
|
||||
TextEntry::make('provider')
|
||||
->label('Provider')
|
||||
->inlineLabel(),
|
||||
TextEntry::make('tags')
|
||||
->label('Tags')
|
||||
->inlineLabel()
|
||||
->state(fn (Server $record) => view('web.components.tags', ['tags' => $record->tags]))
|
||||
->suffixAction(
|
||||
Action::make('edit-tags')
|
||||
->icon('heroicon-o-pencil')
|
||||
->tooltip('Edit Tags')
|
||||
->action(fn (Server $record) => $this->dispatch('$editTags', $record))
|
||||
->tooltip('Edit Tags')
|
||||
),
|
||||
|
||||
]),
|
||||
])
|
||||
->record($this->server);
|
||||
}
|
||||
}
|
73
app/Web/Pages/Servers/Widgets/ServerSummary.php
Normal file
73
app/Web/Pages/Servers/Widgets/ServerSummary.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Web\Pages\Servers\View;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Infolists\Components\Actions\Action;
|
||||
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\Notifications\Notification;
|
||||
use Filament\Support\Enums\IconPosition;
|
||||
use Filament\Widgets\Widget;
|
||||
|
||||
class ServerSummary extends Widget implements HasForms, HasInfolists
|
||||
{
|
||||
use InteractsWithForms;
|
||||
use InteractsWithInfolists;
|
||||
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static string $view = 'web.components.infolist';
|
||||
|
||||
public Server $server;
|
||||
|
||||
public function infolist(Infolist $infolist): Infolist
|
||||
{
|
||||
return $infolist
|
||||
->schema([
|
||||
Fieldset::make('info')
|
||||
->label('Server Summary')
|
||||
->schema([
|
||||
TextEntry::make('name')
|
||||
->label('Name')
|
||||
->url(fn (Server $record) => View::getUrl(parameters: ['server' => $record])),
|
||||
TextEntry::make('ip')
|
||||
->label('IP Address')
|
||||
->icon('heroicon-o-clipboard-document')
|
||||
->iconPosition(IconPosition::After)
|
||||
->copyable(),
|
||||
TextEntry::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(static function ($state): string {
|
||||
return Server::$statusColors[$state];
|
||||
})
|
||||
->suffixAction(
|
||||
Action::make('check-status')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->tooltip('Check Connection')
|
||||
->action(function (Server $record) {
|
||||
$record = $record->checkConnection();
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->status(Server::$statusColors[$record->status])
|
||||
->title('Server is '.$record->status)
|
||||
->send();
|
||||
})
|
||||
),
|
||||
])
|
||||
->columns(3),
|
||||
])
|
||||
->record($this->server->refresh());
|
||||
}
|
||||
}
|
63
app/Web/Pages/Servers/Widgets/ServersList.php
Normal file
63
app/Web/Pages/Servers/Widgets/ServersList.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Web\Pages\Servers\View;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ViewColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ServersList extends Widget
|
||||
{
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Server::query()->where('project_id', auth()->user()->current_project_id);
|
||||
}
|
||||
|
||||
protected static ?string $heading = '';
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('id')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
ViewColumn::make('tags.name')
|
||||
->label('Tags')
|
||||
->view('web.components.tags')
|
||||
->extraCellAttributes(['class' => 'px-3'])
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (Server $server) => Server::$statusColors[$server->status])
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('created_at_by_timezone')
|
||||
->label('Created At')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getTable(): Table
|
||||
{
|
||||
return $this->table
|
||||
->recordUrl(fn (Server $record) => View::getUrl(parameters: ['server' => $record]))
|
||||
->actions([
|
||||
Action::make('settings')
|
||||
->label('Settings')
|
||||
->icon('heroicon-o-cog-6-tooth')
|
||||
->authorize(fn ($record) => auth()->user()->can('update', $record))
|
||||
->url(fn (Server $record) => '/'),
|
||||
]);
|
||||
}
|
||||
}
|
82
app/Web/Pages/Servers/Widgets/UpdateServerInfo.php
Normal file
82
app/Web/Pages/Servers/Widgets/UpdateServerInfo.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Widgets;
|
||||
|
||||
use App\Actions\Server\EditServer;
|
||||
use App\Models\Server;
|
||||
use Filament\Forms\Components\Actions;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Widgets\Widget;
|
||||
|
||||
class UpdateServerInfo extends Widget implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static string $view = 'web.components.form';
|
||||
|
||||
public Server $server;
|
||||
|
||||
public string $name;
|
||||
|
||||
public string $ip;
|
||||
|
||||
public string $port;
|
||||
|
||||
public function mount(Server $server): void
|
||||
{
|
||||
$this->server = $server;
|
||||
$this->name = $server->name;
|
||||
$this->ip = $server->ip;
|
||||
$this->port = $server->port;
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
->heading('Update Server')
|
||||
->description('You can edit your server\'s information here')
|
||||
->columns(1)
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->rules(EditServer::rules($this->server)['name']),
|
||||
TextInput::make('ip')
|
||||
->label('IP Address')
|
||||
->rules(EditServer::rules($this->server)['ip']),
|
||||
TextInput::make('port')
|
||||
->label('Port')
|
||||
->rules(EditServer::rules($this->server)['port']),
|
||||
])
|
||||
->footerActions([
|
||||
Actions\Action::make('submit')
|
||||
->label('Save')
|
||||
->action(fn () => $this->submit()),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function submit(): void
|
||||
{
|
||||
$this->authorize('update', $this->server);
|
||||
|
||||
$this->validate();
|
||||
|
||||
app(EditServer::class)->edit($this->server, $this->all());
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Server updated!')
|
||||
->send();
|
||||
}
|
||||
}
|
33
app/Web/Pages/Settings/Profile/Index.php
Normal file
33
app/Web/Pages/Settings/Profile/Index.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\Profile;
|
||||
|
||||
use App\Web\Pages\Settings\Profile\Widgets\ProfileInformation;
|
||||
use App\Web\Pages\Settings\Profile\Widgets\TwoFactor;
|
||||
use App\Web\Pages\Settings\Profile\Widgets\UpdatePassword;
|
||||
use App\Web\Traits\PageHasWidgets;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
use PageHasWidgets;
|
||||
|
||||
protected static ?string $navigationGroup = 'Settings';
|
||||
|
||||
protected static ?string $slug = 'settings/profile';
|
||||
|
||||
protected static ?string $title = 'Profile';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-user-circle';
|
||||
|
||||
protected static ?int $navigationSort = 2;
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[ProfileInformation::class],
|
||||
[UpdatePassword::class],
|
||||
[TwoFactor::class],
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\Profile\Widgets;
|
||||
|
||||
use App\Actions\User\UpdateUserProfileInformation;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Widgets\Widget;
|
||||
|
||||
class ProfileInformation extends Widget implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static string $view = 'web.components.form';
|
||||
|
||||
public string $name;
|
||||
|
||||
public string $email;
|
||||
|
||||
public string $timezone;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->name = auth()->user()->name;
|
||||
$this->email = auth()->user()->email;
|
||||
$this->timezone = auth()->user()->timezone;
|
||||
}
|
||||
|
||||
public function getFormSchema(): array
|
||||
{
|
||||
$rules = UpdateUserProfileInformation::rules(auth()->user());
|
||||
|
||||
return [
|
||||
Section::make()
|
||||
->heading('Profile Information')
|
||||
->description('Update your account\'s profile information and email address.')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->rules($rules['name']),
|
||||
TextInput::make('email')
|
||||
->label('Email')
|
||||
->rules($rules['email']),
|
||||
Select::make('timezone')
|
||||
->label('Timezone')
|
||||
->searchable()
|
||||
->options(
|
||||
collect(timezone_identifiers_list())
|
||||
->mapWithKeys(fn ($timezone) => [$timezone => $timezone])
|
||||
)
|
||||
->rules($rules['timezone']),
|
||||
])
|
||||
->footerActions([
|
||||
Action::make('save')
|
||||
->label('Save')
|
||||
->action(fn () => $this->submit()),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
public function submit(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
app(UpdateUserProfileInformation::class)->update(auth()->user(), $this->all());
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Profile updated!')
|
||||
->send();
|
||||
}
|
||||
}
|
138
app/Web/Pages/Settings/Profile/Widgets/TwoFactor.php
Normal file
138
app/Web/Pages/Settings/Profile/Widgets/TwoFactor.php
Normal file
@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\Profile\Widgets;
|
||||
|
||||
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\Components\ViewEntry;
|
||||
use Filament\Infolists\Concerns\InteractsWithInfolists;
|
||||
use Filament\Infolists\Contracts\HasInfolists;
|
||||
use Filament\Infolists\Infolist;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Widgets\Widget;
|
||||
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
|
||||
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
|
||||
use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
|
||||
|
||||
class TwoFactor extends Widget implements HasForms, HasInfolists
|
||||
{
|
||||
use InteractsWithForms;
|
||||
use InteractsWithInfolists;
|
||||
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static string $view = 'web.components.infolist';
|
||||
|
||||
public bool $enabled = false;
|
||||
|
||||
public bool $showCodes = false;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
if (auth()->user()->two_factor_secret) {
|
||||
$this->enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function infolist(Infolist $infolist): Infolist
|
||||
{
|
||||
return $infolist->schema([
|
||||
Section::make()
|
||||
->heading('Two Factor Authentication')
|
||||
->description('Here you can activate 2FA to secure your account')
|
||||
->schema([
|
||||
TextEntry::make('disabled')
|
||||
->hiddenLabel()
|
||||
->state('Two factor authentication is disabled.')
|
||||
->visible(! $this->enabled),
|
||||
ViewEntry::make('qr_code')
|
||||
->hiddenLabel()
|
||||
->view('web.components.container', [
|
||||
'content' => $this->enabled ? auth()->user()->twoFactorQrCodeSvg() : null,
|
||||
])
|
||||
->visible($this->enabled && $this->showCodes),
|
||||
TextEntry::make('qr_code_manual')
|
||||
->label('If you are unable to scan the QR code, please use the 2FA secret instead.')
|
||||
->state($this->enabled ? decrypt(auth()->user()->two_factor_secret) : null)
|
||||
->copyable()
|
||||
->visible($this->enabled && $this->showCodes),
|
||||
TextEntry::make('recovery_codes_text')
|
||||
->hiddenLabel()
|
||||
->color('warning')
|
||||
->state('Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.')
|
||||
->visible($this->enabled),
|
||||
ViewEntry::make('recovery_codes')
|
||||
->hiddenLabel()
|
||||
->extraAttributes(['class' => 'rounded-lg border border-gray-100 p-2 dark:border-gray-700'])
|
||||
->view('web.components.container', [
|
||||
'content' => $this->enabled ? implode('</br>', json_decode(decrypt(auth()->user()->two_factor_recovery_codes), true)) : null,
|
||||
])
|
||||
->visible($this->enabled),
|
||||
])
|
||||
->footerActions([
|
||||
Action::make('two-factor')
|
||||
->color($this->enabled ? 'danger' : 'primary')
|
||||
->label($this->enabled ? 'Disable' : 'Enable')
|
||||
->action(function () {
|
||||
if ($this->enabled) {
|
||||
$this->disableTwoFactor();
|
||||
} else {
|
||||
$this->enableTwoFactor();
|
||||
}
|
||||
}),
|
||||
Action::make('regenerate')
|
||||
->color('gray')
|
||||
->label('Regenerate Recovery Codes')
|
||||
->visible($this->enabled)
|
||||
->action(fn () => $this->regenerateRecoveryCodes()),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function enableTwoFactor(): void
|
||||
{
|
||||
app(EnableTwoFactorAuthentication::class)(auth()->user());
|
||||
|
||||
$this->enabled = true;
|
||||
$this->showCodes = true;
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Two factor authentication enabled')
|
||||
->send();
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}
|
||||
|
||||
public function disableTwoFactor(): void
|
||||
{
|
||||
app(DisableTwoFactorAuthentication::class)(auth()->user());
|
||||
|
||||
$this->enabled = false;
|
||||
$this->showCodes = false;
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Two factor authentication disabled')
|
||||
->send();
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}
|
||||
|
||||
public function regenerateRecoveryCodes(): void
|
||||
{
|
||||
app(GenerateNewRecoveryCodes::class)(auth()->user());
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Recovery codes generated')
|
||||
->send();
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}
|
||||
}
|
73
app/Web/Pages/Settings/Profile/Widgets/UpdatePassword.php
Normal file
73
app/Web/Pages/Settings/Profile/Widgets/UpdatePassword.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\Profile\Widgets;
|
||||
|
||||
use App\Actions\User\UpdateUserPassword;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Widgets\Widget;
|
||||
|
||||
class UpdatePassword extends Widget implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static string $view = 'web.components.form';
|
||||
|
||||
public string $current_password = '';
|
||||
|
||||
public string $password = '';
|
||||
|
||||
public string $password_confirmation = '';
|
||||
|
||||
public function getFormSchema(): array
|
||||
{
|
||||
$rules = UpdateUserPassword::rules();
|
||||
|
||||
return [
|
||||
Section::make()
|
||||
->heading('Update Password')
|
||||
->description('Ensure your account is using a long, random password to stay secure.')
|
||||
->schema([
|
||||
TextInput::make('current_password')
|
||||
->label('Current Password')
|
||||
->password()
|
||||
->rules($rules['current_password']),
|
||||
TextInput::make('password')
|
||||
->label('New Password')
|
||||
->password()
|
||||
->rules($rules['password']),
|
||||
TextInput::make('password_confirmation')
|
||||
->label('Confirm Password')
|
||||
->password()
|
||||
->rules($rules['password_confirmation']),
|
||||
])
|
||||
->footerActions([
|
||||
Action::make('save')
|
||||
->label('Save')
|
||||
->action(fn () => $this->submit()),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
public function submit(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
app(UpdateUserPassword::class)->update(auth()->user(), $this->all());
|
||||
|
||||
$this->current_password = '';
|
||||
$this->password = '';
|
||||
$this->password_confirmation = '';
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Password updated!')
|
||||
->send();
|
||||
}
|
||||
}
|
66
app/Web/Pages/Settings/Projects/Index.php
Normal file
66
app/Web/Pages/Settings/Projects/Index.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\Projects;
|
||||
|
||||
use App\Actions\Projects\CreateProject;
|
||||
use App\Models\Project;
|
||||
use App\Web\Traits\PageHasWidgets;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
use PageHasWidgets;
|
||||
|
||||
protected static ?string $navigationGroup = 'Settings';
|
||||
|
||||
protected static ?string $slug = 'settings/projects';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
|
||||
protected static ?int $navigationSort = 4;
|
||||
|
||||
protected static ?string $title = 'Projects';
|
||||
|
||||
public static function getNavigationItemActiveRoutePattern(): string
|
||||
{
|
||||
return static::getRouteName().'*';
|
||||
}
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()?->can('viewAny', Project::class) ?? false;
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\ProjectsList::class],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label('Create Project')
|
||||
->icon('heroicon-o-plus')
|
||||
->authorize('create', Project::class)
|
||||
->using(function (array $data) {
|
||||
return app(CreateProject::class)->create(auth()->user(), $data);
|
||||
})
|
||||
->form(function (Form $form) {
|
||||
return $form->schema([
|
||||
TextInput::make('name')
|
||||
->name('name')
|
||||
->rules(CreateProject::rules()['name']),
|
||||
])->columns(1);
|
||||
})
|
||||
->createAnother(false)
|
||||
->modalWidth(MaxWidth::Large),
|
||||
];
|
||||
}
|
||||
}
|
79
app/Web/Pages/Settings/Projects/Settings.php
Normal file
79
app/Web/Pages/Settings/Projects/Settings.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\Projects;
|
||||
|
||||
use App\Actions\Projects\DeleteProject;
|
||||
use App\Models\Project;
|
||||
use App\Web\Pages\Settings\Projects\Widgets\AddUser;
|
||||
use App\Web\Pages\Settings\Projects\Widgets\ProjectUsersList;
|
||||
use App\Web\Pages\Settings\Projects\Widgets\UpdateProject;
|
||||
use App\Web\Traits\PageHasWidgets;
|
||||
use Exception;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
|
||||
class Settings extends Page
|
||||
{
|
||||
use PageHasWidgets;
|
||||
|
||||
protected static ?string $slug = 'settings/projects/{project}';
|
||||
|
||||
protected static ?string $title = 'Project Settings';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()?->can('update', request()->route('project')) ?? false;
|
||||
}
|
||||
|
||||
public Project $project;
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
UpdateProject::class,
|
||||
['project' => $this->project],
|
||||
],
|
||||
[
|
||||
AddUser::class,
|
||||
['project' => $this->project],
|
||||
],
|
||||
[
|
||||
ProjectUsersList::class,
|
||||
['project' => $this->project],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getTitle(): string|Htmlable
|
||||
{
|
||||
return 'Project Settings';
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make()
|
||||
->record($this->project)
|
||||
->label('Delete Project')
|
||||
->modalHeading('Delete Project')
|
||||
->modalDescription('Are you sure you want to delete this project? This action will delete all associated data and cannot be undone.')
|
||||
->using(function (Project $record) {
|
||||
try {
|
||||
app(DeleteProject::class)->delete(auth()->user(), $record);
|
||||
|
||||
$this->redirectRoute('filament.app.resources.projects.index');
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
73
app/Web/Pages/Settings/Projects/Widgets/AddUser.php
Normal file
73
app/Web/Pages/Settings/Projects/Widgets/AddUser.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\Projects\Widgets;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Widgets\Widget;
|
||||
|
||||
class AddUser extends Widget implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static string $view = 'web.components.form';
|
||||
|
||||
public Project $project;
|
||||
|
||||
public ?int $user;
|
||||
|
||||
public function mount(Project $project): void
|
||||
{
|
||||
$this->project = $project;
|
||||
}
|
||||
|
||||
public function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
Section::make()
|
||||
->heading('Add User')
|
||||
->schema([
|
||||
Select::make('user')
|
||||
->name('user')
|
||||
->options(fn () => User::query()->pluck('name', 'id'))
|
||||
->searchable()
|
||||
->rules(\App\Actions\Projects\AddUser::rules($this->project)['user']),
|
||||
])
|
||||
->footerActions([
|
||||
Action::make('add')
|
||||
->label('Add')
|
||||
->action(fn () => $this->submit()),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
public function submit(): void
|
||||
{
|
||||
$this->authorize('update', $this->project);
|
||||
|
||||
$this->validate();
|
||||
|
||||
app(\App\Actions\Projects\AddUser::class)
|
||||
->add($this->project, [
|
||||
'user' => $this->user,
|
||||
]);
|
||||
|
||||
Notification::make()
|
||||
->title('User added!')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$this->user = null;
|
||||
}
|
||||
|
||||
public function updated(): void
|
||||
{
|
||||
$this->dispatch('userAdded');
|
||||
}
|
||||
}
|
53
app/Web/Pages/Settings/Projects/Widgets/ProjectUsersList.php
Normal file
53
app/Web/Pages/Settings/Projects/Widgets/ProjectUsersList.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\Projects\Widgets;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ProjectUsersList extends Widget
|
||||
{
|
||||
protected $listeners = ['userAdded' => '$refresh'];
|
||||
|
||||
public Project $project;
|
||||
|
||||
public function mount(Project $project): void
|
||||
{
|
||||
$this->project = $project;
|
||||
}
|
||||
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return User::query()->whereHas('projects', function (Builder $query) {
|
||||
$query->where('project_id', $this->project->id);
|
||||
});
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
Tables\Columns\TextColumn::make('id')->width('20%'),
|
||||
Tables\Columns\TextColumn::make('name')->width('20%'),
|
||||
Tables\Columns\TextColumn::make('email')->width('20%'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getTable(): Table
|
||||
{
|
||||
return $this->table->actions([
|
||||
Tables\Actions\DeleteAction::make()
|
||||
->label('Remove')
|
||||
->modalHeading('Remove user from project')
|
||||
->visible(function ($record) {
|
||||
return $this->authorize('update', $this->project)->allowed() && $record->id !== auth()->id();
|
||||
})
|
||||
->using(function ($record) {
|
||||
$this->project->users()->detach($record);
|
||||
}),
|
||||
])->paginated(false);
|
||||
}
|
||||
}
|
47
app/Web/Pages/Settings/Projects/Widgets/ProjectsList.php
Normal file
47
app/Web/Pages/Settings/Projects/Widgets/ProjectsList.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\Projects\Widgets;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Web\Pages\Settings\Projects\Settings;
|
||||
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 ProjectsList extends Widget
|
||||
{
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Project::query();
|
||||
}
|
||||
|
||||
protected static ?string $heading = '';
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('created_at_by_timezone')
|
||||
->label('Created At')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getTable(): Table
|
||||
{
|
||||
return $this->table
|
||||
->recordUrl(fn (Project $record) => Settings::getUrl(['project' => $record]))
|
||||
->actions([
|
||||
Action::make('settings')
|
||||
->label('Settings')
|
||||
->icon('heroicon-o-cog-6-tooth')
|
||||
->authorize(fn ($record) => auth()->user()->can('update', $record))
|
||||
->url(fn (Project $record) => Settings::getUrl(['project' => $record])),
|
||||
]);
|
||||
}
|
||||
}
|
61
app/Web/Pages/Settings/Projects/Widgets/SelectProject.php
Normal file
61
app/Web/Pages/Settings/Projects/Widgets/SelectProject.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\Projects\Widgets;
|
||||
|
||||
use App\Models\Project;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Widgets\Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class SelectProject extends Widget implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static string $view = 'web.components.form';
|
||||
|
||||
public int|string|null $project;
|
||||
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
$options = Project::query()
|
||||
->where(function (Builder $query) {
|
||||
if (auth()->user()->isAdmin()) {
|
||||
return;
|
||||
}
|
||||
$query->where('user_id', auth()->id())
|
||||
->orWhereHas('users', fn ($query) => $query->where('user_id', auth()->id()));
|
||||
})
|
||||
->get()
|
||||
->mapWithKeys(fn ($project) => [$project->id => $project->name])
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
Select::make('project')
|
||||
->name('project')
|
||||
->model($this->project)
|
||||
->label('Project')
|
||||
->searchable()
|
||||
->options($options)
|
||||
->searchPrompt('Select a project...')
|
||||
->extraAttributes(['class' => '-mx-2 pointer-choices'])
|
||||
->selectablePlaceholder(false)
|
||||
->live(),
|
||||
];
|
||||
}
|
||||
|
||||
public function updatedProject($value): void
|
||||
{
|
||||
$project = Project::query()->findOrFail($value);
|
||||
$this->authorize('view', $project);
|
||||
auth()->user()->update(['current_project_id' => $value]);
|
||||
|
||||
$this->redirect('/app');
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->project = auth()->user()->current_project_id;
|
||||
}
|
||||
}
|
66
app/Web/Pages/Settings/Projects/Widgets/UpdateProject.php
Normal file
66
app/Web/Pages/Settings/Projects/Widgets/UpdateProject.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\Projects\Widgets;
|
||||
|
||||
use App\Models\Project;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Widgets\Widget;
|
||||
|
||||
class UpdateProject extends Widget implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static string $view = 'web.components.form';
|
||||
|
||||
public Project $project;
|
||||
|
||||
public string $name;
|
||||
|
||||
public function mount(Project $project): void
|
||||
{
|
||||
$this->project = $project;
|
||||
$this->name = $project->name;
|
||||
}
|
||||
|
||||
public function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
Section::make()
|
||||
->heading('Project Information')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->name('name')
|
||||
->label('Name')
|
||||
->rules(\App\Actions\Projects\UpdateProject::rules($this->project)['name'])
|
||||
->placeholder('Enter the project name'),
|
||||
])
|
||||
->footerActions([
|
||||
Action::make('save')
|
||||
->label('Save')
|
||||
->action(fn () => $this->submit()),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
public function submit(): void
|
||||
{
|
||||
$this->authorize('update', $this->project);
|
||||
|
||||
$this->validate();
|
||||
|
||||
app(\App\Actions\Projects\UpdateProject::class)
|
||||
->update($this->project, [
|
||||
'name' => $this->name,
|
||||
]);
|
||||
|
||||
Notification::make()
|
||||
->title('Project updated successfully!')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
}
|
70
app/Web/Pages/Settings/ServerProviders/Actions/Create.php
Normal file
70
app/Web/Pages/Settings/ServerProviders/Actions/Create.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\ServerProviders\Actions;
|
||||
|
||||
use App\Actions\ServerProvider\CreateServerProvider;
|
||||
use App\Enums\ServerProvider;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
|
||||
class Create
|
||||
{
|
||||
public static function form(): array
|
||||
{
|
||||
return [
|
||||
Select::make('provider')
|
||||
->options(
|
||||
collect(config('core.server_providers'))
|
||||
->filter(fn ($provider) => $provider != ServerProvider::CUSTOM)
|
||||
->mapWithKeys(fn ($provider) => [$provider => $provider])
|
||||
)
|
||||
->live()
|
||||
->reactive()
|
||||
->rules(CreateServerProvider::rules()['provider']),
|
||||
TextInput::make('name')
|
||||
->rules(CreateServerProvider::rules()['name']),
|
||||
TextInput::make('token')
|
||||
->label('API Key')
|
||||
->validationAttribute('API Key')
|
||||
->visible(fn ($get) => in_array($get('provider'), [
|
||||
ServerProvider::DIGITALOCEAN,
|
||||
ServerProvider::LINODE,
|
||||
ServerProvider::VULTR,
|
||||
ServerProvider::HETZNER,
|
||||
]))
|
||||
->rules(fn ($get) => CreateServerProvider::providerRules($get())['token']),
|
||||
TextInput::make('key')
|
||||
->label('Access Key')
|
||||
->visible(function ($get) {
|
||||
return $get('provider') == ServerProvider::AWS;
|
||||
})
|
||||
->rules(fn ($get) => CreateServerProvider::providerRules($get())['key']),
|
||||
TextInput::make('secret')
|
||||
->label('Secret')
|
||||
->visible(fn ($get) => $get('provider') == ServerProvider::AWS)
|
||||
->rules(fn ($get) => CreateServerProvider::providerRules($get())['secret']),
|
||||
Checkbox::make('global')
|
||||
->label('Is Global (Accessible in all projects)'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function action(array $data): void
|
||||
{
|
||||
try {
|
||||
app(CreateServerProvider::class)->create(auth()->user(), $data);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
27
app/Web/Pages/Settings/ServerProviders/Actions/Edit.php
Normal file
27
app/Web/Pages/Settings/ServerProviders/Actions/Edit.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\ServerProviders\Actions;
|
||||
|
||||
use App\Actions\ServerProvider\EditServerProvider;
|
||||
use App\Models\ServerProvider;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
|
||||
class Edit
|
||||
{
|
||||
public static function form(): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->rules(EditServerProvider::rules()['name']),
|
||||
Checkbox::make('global')
|
||||
->label('Is Global (Accessible in all projects)'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function action(ServerProvider $provider, array $data): void
|
||||
{
|
||||
app(EditServerProvider::class)->edit($provider, auth()->user(), $data);
|
||||
}
|
||||
}
|
51
app/Web/Pages/Settings/ServerProviders/Index.php
Normal file
51
app/Web/Pages/Settings/ServerProviders/Index.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\ServerProviders;
|
||||
|
||||
use App\Enums\ServerProvider;
|
||||
use App\Web\Traits\PageHasWidgets;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
use PageHasWidgets;
|
||||
|
||||
protected static ?string $navigationGroup = 'Settings';
|
||||
|
||||
protected static ?string $slug = 'settings/server-providers';
|
||||
|
||||
protected static ?string $title = 'Server Providers';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-server-stack';
|
||||
|
||||
protected static ?int $navigationSort = 5;
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()?->can('viewAny', ServerProvider::class) ?? false;
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\ServerProvidersList::class],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label('Connect')
|
||||
->modalHeading('Connect to a Server Provider')
|
||||
->modalSubmitActionLabel('Connect')
|
||||
->createAnother(false)
|
||||
->form(Actions\Create::form())
|
||||
->authorize('create', ServerProvider::class)
|
||||
->modalWidth(MaxWidth::Medium)
|
||||
->using(fn (array $data) => Actions\Create::action($data)),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\ServerProviders\Widgets;
|
||||
|
||||
use App\Actions\ServerProvider\DeleteServerProvider;
|
||||
use App\Models\ServerProvider;
|
||||
use App\Web\Pages\Settings\ServerProviders\Actions\Edit;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ServerProvidersList extends Widget
|
||||
{
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return ServerProvider::getByProjectId(auth()->user()->current_project_id);
|
||||
}
|
||||
|
||||
protected static ?string $heading = '';
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
ImageColumn::make('image_url')
|
||||
->label('Provider')
|
||||
->size(24),
|
||||
TextColumn::make('name')
|
||||
->default(fn ($record) => $record->profile)
|
||||
->label('Name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('id')
|
||||
->label('Global')
|
||||
->badge()
|
||||
->color(fn ($record) => $record->project_id ? 'gray' : 'success')
|
||||
->formatStateUsing(function (ServerProvider $record) {
|
||||
return $record->project_id ? 'No' : 'Yes';
|
||||
}),
|
||||
TextColumn::make('created_at_by_timezone')
|
||||
->label('Created At')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getTable(): Table
|
||||
{
|
||||
return $this->table->actions([
|
||||
EditAction::make('edit')
|
||||
->label('Edit')
|
||||
->modalHeading('Edit Server Provider')
|
||||
->mutateRecordDataUsing(function (array $data, ServerProvider $record) {
|
||||
return [
|
||||
'name' => $record->profile,
|
||||
'global' => $record->project_id === null,
|
||||
];
|
||||
})
|
||||
->form(Edit::form())
|
||||
->authorize(fn (ServerProvider $record) => auth()->user()->can('update', $record))
|
||||
->using(fn (array $data, ServerProvider $record) => Edit::action($record, $data))
|
||||
->modalWidth(MaxWidth::Medium),
|
||||
DeleteAction::make('delete')
|
||||
->label('Delete')
|
||||
->modalHeading('Delete Server Provider')
|
||||
->authorize(fn (ServerProvider $record) => auth()->user()->can('delete', $record))
|
||||
->using(function (array $data, ServerProvider $record) {
|
||||
app(DeleteServerProvider::class)->delete($record);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
73
app/Web/Pages/Settings/Users/Index.php
Normal file
73
app/Web/Pages/Settings/Users/Index.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\Users;
|
||||
|
||||
use App\Actions\User\CreateUser;
|
||||
use App\Models\User;
|
||||
use App\Web\Traits\PageHasWidgets;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
use PageHasWidgets;
|
||||
|
||||
protected static ?string $navigationGroup = 'Settings';
|
||||
|
||||
protected static ?string $slug = 'users';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-users';
|
||||
|
||||
protected static ?int $navigationSort = 3;
|
||||
|
||||
protected static ?string $title = 'Users';
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()?->can('viewAny', User::class) ?? false;
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\UsersList::class],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label('Create User')
|
||||
->authorize('create', User::class)
|
||||
->action(function (array $data) {
|
||||
$user = app(CreateUser::class)->create($data);
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
return $user;
|
||||
})
|
||||
->form(function (Form $form) {
|
||||
$rules = CreateUser::rules();
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->rules($rules['name']),
|
||||
TextInput::make('email')
|
||||
->rules($rules['email']),
|
||||
TextInput::make('password')
|
||||
->rules($rules['password']),
|
||||
Select::make('role')
|
||||
->rules($rules['role'])
|
||||
->options(collect(config('core.user_roles'))->mapWithKeys(fn ($role) => [$role => $role])),
|
||||
])
|
||||
->columns(1);
|
||||
})
|
||||
->modalWidth(MaxWidth::Large),
|
||||
];
|
||||
}
|
||||
}
|
113
app/Web/Pages/Settings/Users/Widgets/UsersList.php
Normal file
113
app/Web/Pages/Settings/Users/Widgets/UsersList.php
Normal file
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\Users\Widgets;
|
||||
|
||||
use App\Actions\User\UpdateProjects;
|
||||
use App\Actions\User\UpdateUser;
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
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;
|
||||
|
||||
class UsersList extends Widget
|
||||
{
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return User::query();
|
||||
}
|
||||
|
||||
protected static ?string $heading = '';
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('email')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('timezone'),
|
||||
TextColumn::make('created_at_by_timezone')
|
||||
->label('Created At')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('role'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getTable(): Table
|
||||
{
|
||||
return $this->table
|
||||
->actions([
|
||||
EditAction::make('edit')
|
||||
->authorize(fn ($record) => auth()->user()->can('update', $record))
|
||||
->using(function ($record, array $data) {
|
||||
app(UpdateUser::class)->update($record, $data);
|
||||
})
|
||||
->form(function (Form $form, $record) {
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->rules(UpdateUser::rules($record)['name']),
|
||||
TextInput::make('email')
|
||||
->rules(UpdateUser::rules($record)['email']),
|
||||
Select::make('timezone')
|
||||
->searchable()
|
||||
->options(
|
||||
collect(timezone_identifiers_list())
|
||||
->mapWithKeys(fn ($timezone) => [$timezone => $timezone])
|
||||
)
|
||||
->rules(UpdateUser::rules($record)['timezone']),
|
||||
Select::make('role')
|
||||
->options(
|
||||
collect(config('core.user_roles'))
|
||||
->mapWithKeys(fn ($role) => [$role => $role])
|
||||
)
|
||||
->rules(UpdateUser::rules($record)['role']),
|
||||
])
|
||||
->columns(1);
|
||||
})
|
||||
->modalWidth(MaxWidth::Large),
|
||||
Action::make('update-projects')
|
||||
->label('Projects')
|
||||
->icon('heroicon-o-rectangle-stack')
|
||||
->authorize(fn ($record) => auth()->user()->can('update', $record))
|
||||
->action(function ($record, array $data) {
|
||||
app(UpdateProjects::class)->update($record, $data);
|
||||
Notification::make()
|
||||
->title('Projects Updated')
|
||||
->success()
|
||||
->send();
|
||||
})
|
||||
->form(function (Form $form, $record) {
|
||||
return $form
|
||||
->schema([
|
||||
CheckboxList::make('projects')
|
||||
->options(Project::query()->pluck('name', 'id')->toArray())
|
||||
->searchable()
|
||||
->default($record->projects->pluck('id')->toArray())
|
||||
->rules(UpdateProjects::rules()['projects.*']),
|
||||
])
|
||||
->columns(1);
|
||||
})
|
||||
->modalSubmitActionLabel('Save')
|
||||
->modalWidth(MaxWidth::Large),
|
||||
DeleteAction::make()
|
||||
->authorize(fn ($record) => auth()->user()->can('delete', $record)),
|
||||
]);
|
||||
}
|
||||
}
|
72
app/Web/Traits/BelongsToServers.php
Normal file
72
app/Web/Traits/BelongsToServers.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Traits;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
trait BelongsToServers
|
||||
{
|
||||
public static function getUrl(string $name = 'index', array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string
|
||||
{
|
||||
if (! isset($parameters['server'])) {
|
||||
$parameters['server'] = request()->route('server') ?? 0;
|
||||
}
|
||||
|
||||
return parent::getUrl($name, $parameters, $isAbsolute, $panel, $tenant);
|
||||
}
|
||||
|
||||
public static function getServerFromRoute(): Server
|
||||
{
|
||||
$server = request()->route('server');
|
||||
|
||||
if (! $server) {
|
||||
$server = Route::getRoutes()->match(Request::create(url()->previous()))->parameter('server');
|
||||
}
|
||||
|
||||
if (! $server instanceof Server) {
|
||||
$server = Server::query()->find($server);
|
||||
}
|
||||
|
||||
if (! $server) {
|
||||
$server = new Server;
|
||||
}
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return static::can('viewAny');
|
||||
|
||||
return auth()->user()->can('viewAny', [static::getModel(), static::getServerFromRoute()]);
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return auth()->user()->can('create', [static::getModel(), static::getServerFromRoute()]);
|
||||
}
|
||||
|
||||
public static function authorizeViewAny(): void
|
||||
{
|
||||
Gate::authorize('viewAny', [static::getModel(), static::getServerFromRoute()]);
|
||||
}
|
||||
|
||||
public static function authorizeCreate(): void
|
||||
{
|
||||
Gate::authorize('create', [static::getModel(), static::getServerFromRoute()]);
|
||||
}
|
||||
|
||||
public static function authorizeEdit(Model $record): void
|
||||
{
|
||||
Gate::authorize('update', [$record, static::getServerFromRoute()]);
|
||||
}
|
||||
|
||||
public static function authorizeView(Model $record): void
|
||||
{
|
||||
Gate::authorize('view', [$record, static::getServerFromRoute()]);
|
||||
}
|
||||
}
|
20
app/Web/Traits/PageHasCluster.php
Normal file
20
app/Web/Traits/PageHasCluster.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Traits;
|
||||
|
||||
trait PageHasCluster
|
||||
{
|
||||
// public function getMaxContentWidth(): ?string
|
||||
// {
|
||||
// return 'full';
|
||||
// }
|
||||
|
||||
public function getSubNavigation(): array
|
||||
{
|
||||
if (filled($cluster = static::getCluster())) {
|
||||
return $this->generateNavigationItems($cluster::getClusteredComponents());
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
61
app/Web/Traits/PageHasServer.php
Normal file
61
app/Web/Traits/PageHasServer.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Traits;
|
||||
|
||||
use App\Models\Site;
|
||||
use App\Web\Pages\Servers\Settings;
|
||||
use App\Web\Pages\Servers\Sites\Index;
|
||||
use App\Web\Pages\Servers\View;
|
||||
use App\Web\Pages\Servers\Widgets\ServerSummary;
|
||||
use Filament\Navigation\NavigationItem;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
|
||||
trait PageHasServer
|
||||
{
|
||||
public function getTitle(): string|Htmlable
|
||||
{
|
||||
return static::$title.' - '.$this->server->name;
|
||||
}
|
||||
|
||||
public function getSubNavigation(): array
|
||||
{
|
||||
$items = [];
|
||||
|
||||
if (auth()->user()?->can('view', $this->server)) {
|
||||
$items[] = NavigationItem::make('Overview')
|
||||
->icon('heroicon-o-chart-pie')
|
||||
->isActiveWhen(fn () => request()->routeIs(View::getRouteName()))
|
||||
->url(View::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if (auth()->user()?->can('viewAny', [Site::class, $this->server])) {
|
||||
$items[] = NavigationItem::make('Sites')
|
||||
->icon('heroicon-o-globe-alt')
|
||||
->isActiveWhen(fn () => request()->routeIs(Index::getRouteName().'*'))
|
||||
->url(Index::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
||||
if (auth()->user()?->can('update', $this->server)) {
|
||||
$items[] = NavigationItem::make('Settings')
|
||||
->icon('heroicon-o-cog-6-tooth')
|
||||
->isActiveWhen(fn () => request()->routeIs(Settings::getRouteName().'*'))
|
||||
->url(Settings::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;
|
||||
}
|
||||
}
|
29
app/Web/Traits/PageHasWidgets.php
Normal file
29
app/Web/Traits/PageHasWidgets.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Traits;
|
||||
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
trait PageHasWidgets
|
||||
{
|
||||
protected array $extraAttributes = [
|
||||
'wire:poll.5s' => '$dispatch(\'$refresh\')',
|
||||
];
|
||||
|
||||
protected function getExtraAttributes(): array
|
||||
{
|
||||
return $this->extraAttributes;
|
||||
}
|
||||
|
||||
public function getView(): string
|
||||
{
|
||||
return 'web.components.page';
|
||||
}
|
||||
|
||||
public function getExtraAttributesBag(): ComponentAttributeBag
|
||||
{
|
||||
return new ComponentAttributeBag($this->getExtraAttributes());
|
||||
}
|
||||
|
||||
abstract public function getWidgets(): array;
|
||||
}
|
@ -2,15 +2,13 @@
|
||||
"name": "vitodeploy/vito",
|
||||
"type": "project",
|
||||
"description": "The ultimate server management tool",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"laravel"
|
||||
],
|
||||
"keywords": ["framework", "laravel"],
|
||||
"license": "AGPL-3.0",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"ext-ftp": "*",
|
||||
"aws/aws-sdk-php": "^3.158",
|
||||
"filament/filament": "^3.2",
|
||||
"laravel/fortify": "^1.17",
|
||||
"laravel/framework": "^11.0",
|
||||
"laravel/tinker": "^2.8",
|
||||
@ -18,6 +16,7 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"laradumps/laradumps": "^3.0",
|
||||
"laravel/pint": "^1.10",
|
||||
"laravel/sail": "^1.18",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
@ -43,7 +42,8 @@
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
"@php artisan package:discover --ansi",
|
||||
"@php artisan filament:upgrade"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
|
2957
composer.lock
generated
2957
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -193,6 +193,7 @@
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\AuthServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\WebServiceProvider::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|
@ -476,4 +476,9 @@
|
||||
\App\Models\Server::class,
|
||||
\App\Models\Site::class,
|
||||
],
|
||||
|
||||
'user_roles' => [
|
||||
\App\Enums\UserRole::USER,
|
||||
\App\Enums\UserRole::ADMIN,
|
||||
]
|
||||
];
|
||||
|
89
config/filament.php
Normal file
89
config/filament.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Broadcasting
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By uncommenting the Laravel Echo configuration, you may connect Filament
|
||||
| to any Pusher-compatible websockets server.
|
||||
|
|
||||
| This will allow your users to receive real-time notifications.
|
||||
|
|
||||
*/
|
||||
|
||||
'broadcasting' => [
|
||||
|
||||
// 'echo' => [
|
||||
// 'broadcaster' => 'pusher',
|
||||
// 'key' => env('VITE_PUSHER_APP_KEY'),
|
||||
// 'cluster' => env('VITE_PUSHER_APP_CLUSTER'),
|
||||
// 'wsHost' => env('VITE_PUSHER_HOST'),
|
||||
// 'wsPort' => env('VITE_PUSHER_PORT'),
|
||||
// 'wssPort' => env('VITE_PUSHER_PORT'),
|
||||
// 'authEndpoint' => '/broadcasting/auth',
|
||||
// 'disableStats' => true,
|
||||
// 'encrypted' => true,
|
||||
// 'forceTLS' => true,
|
||||
// ],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the storage disk Filament will use to store files. You may use
|
||||
| any of the disks defined in the `config/filesystems.php`.
|
||||
|
|
||||
*/
|
||||
|
||||
'default_filesystem_disk' => env('FILAMENT_FILESYSTEM_DISK', 'public'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Assets Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the directory where Filament's assets will be published to. It
|
||||
| is relative to the `public` directory of your Laravel application.
|
||||
|
|
||||
| After changing the path, you should run `php artisan filament:assets`.
|
||||
|
|
||||
*/
|
||||
|
||||
'assets_path' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the directory that Filament will use to store cache files that
|
||||
| are used to optimize the registration of components.
|
||||
|
|
||||
| After changing the path, you should run `php artisan filament:cache-components`.
|
||||
|
|
||||
*/
|
||||
|
||||
'cache_path' => base_path('bootstrap/cache/filament'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Livewire Loading Delay
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This sets the delay before loading indicators appear.
|
||||
|
|
||||
| Setting this to 'none' makes indicators appear immediately, which can be
|
||||
| desirable for high-latency connections. Setting it to 'default' applies
|
||||
| Livewire's standard 200ms delay.
|
||||
|
|
||||
*/
|
||||
|
||||
'livewire_loading_delay' => 'default',
|
||||
|
||||
];
|
5
lang/en.json
Normal file
5
lang/en.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"servers.create.public_key_text": "mkdir -p /root/.ssh && touch /root/.ssh/authorized_keys && echo ':public_key' >> /root/.ssh/authorized_keys",
|
||||
"servers.create.public_key_warning": "Your server needs to have a new unused installation of supported operating systems and must have a root user. To get started, add our public key to /root/.ssh/authorized_keys file by running the bellow command on your server as root.",
|
||||
"server_providers.plan": ":name - :cpu Cores(:architecture) - :memory - :disk Disk"
|
||||
}
|
189
package-lock.json
generated
189
package-lock.json
generated
@ -5,22 +5,23 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"alpinejs": "^3.4.2",
|
||||
"apexcharts": "^3.44.2",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"brace": "^0.11.1",
|
||||
"flowbite": "^2.3.0",
|
||||
"flowbite-datepicker": "^1.2.6",
|
||||
"htmx.org": "^1.9.10",
|
||||
"laravel-echo": "^1.15.0",
|
||||
"laravel-vite-plugin": "^0.7.2",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss": "^8.4.45",
|
||||
"postcss-nesting": "^13.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-blade": "^2.1.6",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"tailwindcss": "^3.1.0",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"tippy.js": "^6.3.7",
|
||||
"vite": "^4.5.3"
|
||||
}
|
||||
@ -510,21 +511,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/forms": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz",
|
||||
"integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==",
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.9.tgz",
|
||||
"integrity": "sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mini-svg-data-uri": "^1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
|
||||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
"version": "0.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.13.tgz",
|
||||
"integrity": "sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==",
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz",
|
||||
"integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash.castarray": "^4.4.0",
|
||||
@ -533,7 +534,7 @@
|
||||
"postcss-selector-parser": "6.0.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.0.0 || insiders"
|
||||
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
@ -631,9 +632,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.19",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
|
||||
"integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
|
||||
"version": "10.4.20",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
||||
"integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -650,11 +651,11 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"browserslist": "^4.23.0",
|
||||
"caniuse-lite": "^1.0.30001599",
|
||||
"browserslist": "^4.23.3",
|
||||
"caniuse-lite": "^1.0.30001646",
|
||||
"fraction.js": "^4.3.7",
|
||||
"normalize-range": "^0.1.2",
|
||||
"picocolors": "^1.0.0",
|
||||
"picocolors": "^1.0.1",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"bin": {
|
||||
@ -713,9 +714,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.23.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
|
||||
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
|
||||
"version": "4.23.3",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
|
||||
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -732,10 +733,10 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001587",
|
||||
"electron-to-chromium": "^1.4.668",
|
||||
"node-releases": "^2.0.14",
|
||||
"update-browserslist-db": "^1.0.13"
|
||||
"caniuse-lite": "^1.0.30001646",
|
||||
"electron-to-chromium": "^1.5.4",
|
||||
"node-releases": "^2.0.18",
|
||||
"update-browserslist-db": "^1.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@ -754,9 +755,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001615",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001615.tgz",
|
||||
"integrity": "sha512-1IpazM5G3r38meiae0bHRnPhz+CBQ3ZLqbQMtrg+AsTPKAXgW38JNsXkyZ+v8waCsDmPq87lmfun5Q2AGysNEQ==",
|
||||
"version": "1.0.30001658",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001658.tgz",
|
||||
"integrity": "sha512-N2YVqWbJELVdrnsW5p+apoQyYt51aBMSsBZki1XZEfeBCexcM/sf4xiAHcXQBkuOwJBXtWF7aW1sYX6tKebPHw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -881,9 +882,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.754",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.754.tgz",
|
||||
"integrity": "sha512-7Kr5jUdns5rL/M9wFFmMZAgFDuL2YOnanFH4OI4iFzUqyh3XOL7nAGbSlSMZdzKMIyyTpNSbqZsWG9odwLeKvA==",
|
||||
"version": "1.5.18",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.18.tgz",
|
||||
"integrity": "sha512-1OfuVACu+zKlmjsNdcJuVQuVE61sZOLbNM4JAQ1Rvh6EOj0/EUKhMJjRH73InPlXSh8HIJk1cVZ8pyOV/FMdUQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
@ -930,9 +931,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@ -1358,9 +1359,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
||||
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
|
||||
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
@ -1431,9 +1432,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
@ -1467,9 +1468,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.38",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
|
||||
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
|
||||
"version": "8.4.45",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz",
|
||||
"integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -1487,7 +1488,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.0",
|
||||
"picocolors": "^1.0.1",
|
||||
"source-map-js": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@ -1609,6 +1610,90 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-nesting": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.0.tgz",
|
||||
"integrity": "sha512-TCGQOizyqvEkdeTPM+t6NYwJ3EJszYE/8t8ILxw/YoeUvz2rz7aM8XTAmBWh9/DJjfaaabL88fWrsVHSPF2zgA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/selector-resolve-nested": "^2.0.0",
|
||||
"@csstools/selector-specificity": "^4.0.0",
|
||||
"postcss-selector-parser": "^6.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-2.0.0.tgz",
|
||||
"integrity": "sha512-oklSrRvOxNeeOW1yARd4WNCs/D09cQjunGZUgSq6vM8GpzFswN+8rBZyJA29YFZhOTQ6GFzxgLDNtVbt9wPZMA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss-selector-parser": "^6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-4.0.0.tgz",
|
||||
"integrity": "sha512-189nelqtPd8++phaHNwYovKZI0FOzH1vQEE3QhHHkNIGrg5fSs9CbYP3RvfEH5geztnIA9Jwq91wyOIwAW5JIQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss-selector-parser": "^6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-nesting/node_modules/postcss-selector-parser": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||
@ -2100,9 +2185,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
|
||||
"integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==",
|
||||
"version": "3.4.10",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
|
||||
"integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
@ -2198,9 +2283,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.14.tgz",
|
||||
"integrity": "sha512-JixKH8GR2pWYshIPUg/NujK3JO7JiqEEUiNArE86NQyrgUuZeTlZQN3xuS/yiV5Kb48ev9K6RqNkaJjXsdg7Jw==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
|
||||
"integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -2218,7 +2303,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"escalade": "^3.1.2",
|
||||
"picocolors": "^1.0.0"
|
||||
"picocolors": "^1.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"update-browserslist-db": "cli.js"
|
||||
|
11
package.json
11
package.json
@ -7,22 +7,23 @@
|
||||
"lint:fix": "prettier --write ./resources/views"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"alpinejs": "^3.4.2",
|
||||
"apexcharts": "^3.44.2",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"brace": "^0.11.1",
|
||||
"flowbite": "^2.3.0",
|
||||
"flowbite-datepicker": "^1.2.6",
|
||||
"htmx.org": "^1.9.10",
|
||||
"laravel-echo": "^1.15.0",
|
||||
"laravel-vite-plugin": "^0.7.2",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss": "^8.4.45",
|
||||
"postcss-nesting": "^13.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-blade": "^2.1.6",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"tailwindcss": "^3.1.0",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"tippy.js": "^6.3.7",
|
||||
"vite": "^4.5.3"
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,9 +1,14 @@
|
||||
{
|
||||
"resources/css/app.css": {
|
||||
"file": "assets/app-888ea5fa.css",
|
||||
"file": "assets/app-852e0e7b.css",
|
||||
"isEntry": true,
|
||||
"src": "resources/css/app.css"
|
||||
},
|
||||
"resources/css/filament/app/theme.css": {
|
||||
"file": "assets/theme-98a3afe7.css",
|
||||
"isEntry": true,
|
||||
"src": "resources/css/filament/app/theme.css"
|
||||
},
|
||||
"resources/js/app.css": {
|
||||
"file": "assets/app-a1ae07b3.css",
|
||||
"src": "resources/js/app.css"
|
||||
|
1
public/css/filament/filament/app.css
Normal file
1
public/css/filament/filament/app.css
Normal file
File diff suppressed because one or more lines are too long
49
public/css/filament/forms/forms.css
Normal file
49
public/css/filament/forms/forms.css
Normal file
File diff suppressed because one or more lines are too long
1
public/css/filament/support/support.css
Normal file
1
public/css/filament/support/support.css
Normal file
@ -0,0 +1 @@
|
||||
.fi-pagination-items,.fi-pagination-overview,.fi-pagination-records-per-page-select:not(.fi-compact){display:none}@supports (container-type:inline-size){.fi-pagination{container-type:inline-size}@container (min-width: 28rem){.fi-pagination-records-per-page-select.fi-compact{display:none}.fi-pagination-records-per-page-select:not(.fi-compact){display:inline}}@container (min-width: 56rem){.fi-pagination:not(.fi-simple)>.fi-pagination-previous-btn{display:none}.fi-pagination-overview{display:inline}.fi-pagination:not(.fi-simple)>.fi-pagination-next-btn{display:none}.fi-pagination-items{display:flex}}}@supports not (container-type:inline-size){@media (min-width:640px){.fi-pagination-records-per-page-select.fi-compact{display:none}.fi-pagination-records-per-page-select:not(.fi-compact){display:inline}}@media (min-width:768px){.fi-pagination:not(.fi-simple)>.fi-pagination-previous-btn{display:none}.fi-pagination-overview{display:inline}.fi-pagination:not(.fi-simple)>.fi-pagination-next-btn{display:none}.fi-pagination-items{display:flex}}}.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{background-color:#333;border-radius:4px;color:#fff;font-size:14px;line-height:1.4;outline:0;position:relative;transition-property:transform,visibility,opacity;white-space:normal}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{border-top-color:initial;border-width:8px 8px 0;bottom:-7px;left:0;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:initial;border-width:0 8px 8px;left:0;top:-7px;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-left-color:initial;border-width:8px 0 8px 8px;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{border-right-color:initial;border-width:8px 8px 8px 0;left:-7px;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{color:#333;height:16px;width:16px}.tippy-arrow:before{border-color:transparent;border-style:solid;content:"";position:absolute}.tippy-content{padding:5px 9px;position:relative;z-index:1}.tippy-box[data-theme~=light]{background-color:#fff;box-shadow:0 0 20px 4px #9aa1b126,0 4px 80px -8px #24282f40,0 4px 4px -2px #5b5e6926;color:#26323d}.tippy-box[data-theme~=light][data-placement^=top]>.tippy-arrow:before{border-top-color:#fff}.tippy-box[data-theme~=light][data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:#fff}.tippy-box[data-theme~=light][data-placement^=left]>.tippy-arrow:before{border-left-color:#fff}.tippy-box[data-theme~=light][data-placement^=right]>.tippy-arrow:before{border-right-color:#fff}.tippy-box[data-theme~=light]>.tippy-backdrop{background-color:#fff}.tippy-box[data-theme~=light]>.tippy-svg-arrow{fill:#fff}.fi-sortable-ghost{opacity:.3}
|
1
public/js/filament/filament/app.js
Normal file
1
public/js/filament/filament/app.js
Normal file
File diff suppressed because one or more lines are too long
13
public/js/filament/filament/echo.js
Normal file
13
public/js/filament/filament/echo.js
Normal file
File diff suppressed because one or more lines are too long
1
public/js/filament/forms/components/color-picker.js
Normal file
1
public/js/filament/forms/components/color-picker.js
Normal file
File diff suppressed because one or more lines are too long
1
public/js/filament/forms/components/date-time-picker.js
Normal file
1
public/js/filament/forms/components/date-time-picker.js
Normal file
File diff suppressed because one or more lines are too long
123
public/js/filament/forms/components/file-upload.js
Normal file
123
public/js/filament/forms/components/file-upload.js
Normal file
File diff suppressed because one or more lines are too long
1
public/js/filament/forms/components/key-value.js
Normal file
1
public/js/filament/forms/components/key-value.js
Normal file
@ -0,0 +1 @@
|
||||
function r({state:o}){return{state:o,rows:[],shouldUpdateRows:!0,init:function(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(t,e)=>{let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(t)===0&&s(e)===0||this.updateRows()})},addRow:function(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow:function(t){this.rows.splice(t,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows:function(t){let e=Alpine.raw(this.rows);this.rows=[];let s=e.splice(t.oldIndex,1)[0];e.splice(t.newIndex,0,s),this.$nextTick(()=>{this.rows=e,this.updateState()})},updateRows:function(){if(!this.shouldUpdateRows){this.shouldUpdateRows=!0;return}let t=[];for(let[e,s]of Object.entries(this.state??{}))t.push({key:e,value:s});this.rows=t},updateState:function(){let t={};this.rows.forEach(e=>{e.key===""||e.key===null||(t[e.key]=e.value)}),this.shouldUpdateRows=!1,this.state=t}}}export{r as default};
|
51
public/js/filament/forms/components/markdown-editor.js
Normal file
51
public/js/filament/forms/components/markdown-editor.js
Normal file
File diff suppressed because one or more lines are too long
144
public/js/filament/forms/components/rich-editor.js
Normal file
144
public/js/filament/forms/components/rich-editor.js
Normal file
File diff suppressed because one or more lines are too long
6
public/js/filament/forms/components/select.js
Normal file
6
public/js/filament/forms/components/select.js
Normal file
File diff suppressed because one or more lines are too long
1
public/js/filament/forms/components/tags-input.js
Normal file
1
public/js/filament/forms/components/tags-input.js
Normal file
@ -0,0 +1 @@
|
||||
function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{["x-on:blur"]:"createTag()",["x-model"]:"newTag",["x-on:keydown"](t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},["x-on:paste"](){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default};
|
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