Compare commits

..

10 Commits
1.4.0 ... 1.5.0

Author SHA1 Message Date
e704a13d6b new toast notification ui (#188) 2024-05-03 10:40:01 +02:00
9936958259 local storage driver & some icon fixes (#187) 2024-05-01 22:54:44 +02:00
f81d928c66 Update README.md 2024-05-01 12:42:16 +02:00
3c4435701d fix deployment button ux (#186) 2024-05-01 09:34:01 +02:00
ebbd81348a build assets 2024-04-29 21:14:32 +02:00
5debbd4f5d update project lowercase 2024-04-29 21:11:07 +02:00
d846acaa8d User management (#185) 2024-04-29 20:58:04 +02:00
35f896eab1 Update feature_request.md
fix #179
2024-04-24 14:40:26 +02:00
25977d2ead move projects from sidebar to topbar (#170)
and fix #169
2024-04-23 21:34:39 +02:00
f0da1c6d8c Accurate deployment statuses (#168) 2024-04-21 16:26:26 +02:00
167 changed files with 3888 additions and 3265 deletions

View File

@ -9,4 +9,4 @@
To request a feature or suggest an idea please add it to the feedback boards
https://features.vitodeploy.com/
https://vitodeploy.featurebase.app/

View File

@ -1,6 +1,6 @@
<p align="center">
<img alt="srcshot 2024-02-23 at 16 26 21@2x" src="https://github.com/vitodeploy/vito/assets/61919774/9b3ae8fe-996a-4e10-b42e-74097f8e5512" alt="VitoDeploy>
<img src="https://github.com/vitodeploy/vito/assets/61919774/8060fded-58e3-4d58-b58b-5b717b0718e9" alt="VitoDeploy>
<p align="center">
<a href="https://github.com/vitodeploy/vito/actions"><img alt="GitHub Workflow Status" src="https://github.com/vitodeploy/vito/workflows/tests/badge.svg"></a>
</p>
@ -50,8 +50,6 @@ ## Credits
- Alpinejs
- HTMX
- Vite
- Toastr by CodeSeven
- Prettier
- Postcss
- Flowbite
- svgrepo.com

View File

@ -10,10 +10,13 @@ class CreateProject
{
public function create(User $user, array $input): Project
{
if (isset($input['name'])) {
$input['name'] = strtolower($input['name']);
}
$this->validate($user, $input);
$project = new Project([
'user_id' => $user->id,
'name' => $input['name'],
]);
@ -29,7 +32,7 @@ private function validate(User $user, array $input): void
'required',
'string',
'max:255',
'unique:projects,name,NULL,id,user_id,'.$user->id,
'unique:projects,name',
],
])->validate();
}

View File

@ -10,6 +10,10 @@ class UpdateProject
{
public function update(Project $project, array $input): Project
{
if (isset($input['name'])) {
$input['name'] = strtolower($input['name']);
}
$this->validate($project, $input);
$project->name = $input['name'];

View File

@ -6,6 +6,7 @@
use App\Exceptions\DeploymentScriptIsEmptyException;
use App\Exceptions\SourceControlIsNotConnected;
use App\Models\Deployment;
use App\Models\ServerLog;
use App\Models\Site;
class Deploy
@ -37,10 +38,15 @@ public function run(Site $site): Deployment
$deployment->save();
dispatch(function () use ($site, $deployment) {
$log = $site->server->os()->runScript($site->path, $site->deploymentScript->content, $site->id);
$deployment->status = DeploymentStatus::FINISHED;
/** @var ServerLog $log */
$log = ServerLog::make($site->server, 'deploy-'.strtotime('now'))
->forSite($site);
$log->save();
$deployment->log_id = $log->id;
$deployment->save();
$site->server->os()->runScript($site->path, $site->deploymentScript->content, $log);
$deployment->status = DeploymentStatus::FINISHED;
$deployment->save();
})->catch(function () use ($deployment) {
$deployment->status = DeploymentStatus::FAILED;
$deployment->save();

View File

@ -0,0 +1,40 @@
<?php
namespace App\Actions\User;
use App\Enums\UserRole;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class CreateUser
{
public function create(array $input): User
{
$this->validate($input);
/** @var User $user */
$user = User::query()->create([
'name' => $input['name'],
'email' => $input['email'],
'role' => $input['role'],
'password' => bcrypt($input['password']),
'timezone' => 'UTC',
]);
return $user;
}
private function validate(array $input): void
{
Validator::make($input, [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'password' => 'required|string|min:8',
'role' => [
'required',
Rule::in([UserRole::ADMIN, UserRole::USER]),
],
])->validate();
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Actions\User;
use App\Enums\UserRole;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class UpdateUser
{
public function update(User $user, array $input): void
{
$this->validate($user, $input);
$user->name = $input['name'];
$user->email = $input['email'];
$user->timezone = $input['timezone'];
$user->role = $input['role'];
if (isset($input['password']) && $input['password'] !== null) {
$user->password = bcrypt($input['password']);
}
$user->save();
}
private function validate(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()),
],
'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();
}
}

View File

@ -7,7 +7,7 @@
class CreateUserCommand extends Command
{
protected $signature = 'user:create {name} {email} {password}';
protected $signature = 'user:create {name} {email} {password} {--role=admin}';
protected $description = 'Create a new user';
@ -25,6 +25,7 @@ public function handle(): void
'name' => $this->argument('name'),
'email' => $this->argument('email'),
'password' => bcrypt($this->argument('password')),
'role' => $this->option('role'),
]);
$this->info('User created!');

View File

@ -7,4 +7,6 @@ final class StorageProvider
const DROPBOX = 'dropbox';
const FTP = 'ftp';
const LOCAL = 'local';
}

10
app/Enums/UserRole.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace App\Enums;
final class UserRole
{
const USER = 'user';
const ADMIN = 'admin';
}

View File

@ -3,6 +3,7 @@
namespace App\Facades;
use App\Models\Server;
use App\Models\ServerLog;
use App\Support\Testing\SSHFake;
use Illuminate\Support\Facades\Facade as FacadeAlias;
@ -10,7 +11,7 @@
* Class SSH
*
* @method static init(Server $server, string $asUser = null)
* @method static setLog(string $logType, int $siteId = null)
* @method static setLog(?ServerLog $log)
* @method static connect()
* @method static string exec(string $command, string $log = '', int $siteId = null, ?bool $stream = false)
* @method static string assertExecuted(array|string $commands)

View File

@ -50,14 +50,9 @@ public function init(Server $server, ?string $asUser = null): self
return $this;
}
public function setLog(string $logType, $siteId = null): self
public function setLog(ServerLog $log): self
{
$this->log = $this->server->logs()->create([
'site_id' => $siteId,
'name' => $this->server->id.'-'.strtotime('now').'-'.$logType.'.log',
'type' => $logType,
'disk' => config('core.logs_disk'),
]);
$this->log = $log;
return $this;
}
@ -98,10 +93,18 @@ public function connect(bool $sftp = false): void
*/
public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false): string
{
if ($log) {
$this->setLog($log, $siteId);
} else {
$this->log = null;
if (! $this->log && $log) {
$this->log = $this->server->logs()->create([
'site_id' => $siteId,
'name' => $this->server->id.'-'.strtotime('now').'-'.$log.'.log',
'type' => $log,
'disk' => config('core.logs_disk'),
]);
$this->log = ServerLog::make($this->server, $log);
if ($siteId) {
$this->log->forSite($siteId);
}
$this->log->save();
}
try {
@ -132,8 +135,8 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
$this->log?->write($output);
if (Str::contains($output, 'VITO_SSH_ERROR')) {
throw new SSHCommandError('SSH command failed with an error');
if ($this->connection->getExitStatus() !== 0 || Str::contains($output, 'VITO_SSH_ERROR')) {
throw new SSHCommandError('SSH command failed with an error', $this->connection->getExitStatus());
}
return $output;

View File

@ -22,6 +22,8 @@ class ApplicationController extends Controller
{
public function deploy(Server $server, Site $site): HtmxResponse
{
$this->authorize('manage', $server);
try {
app(Deploy::class)->run($site);
@ -41,11 +43,15 @@ public function deploy(Server $server, Site $site): HtmxResponse
public function showDeploymentLog(Server $server, Site $site, Deployment $deployment): RedirectResponse
{
$this->authorize('manage', $server);
return back()->with('content', $deployment->log?->getContent());
}
public function updateDeploymentScript(Server $server, Site $site, Request $request): RedirectResponse
{
$this->authorize('manage', $server);
app(UpdateDeploymentScript::class)->update($site, $request->input());
Toast::success('Deployment script updated!');
@ -55,6 +61,8 @@ public function updateDeploymentScript(Server $server, Site $site, Request $requ
public function updateBranch(Server $server, Site $site, Request $request): RedirectResponse
{
$this->authorize('manage', $server);
app(UpdateBranch::class)->update($site, $request->input());
Toast::success('Branch updated!');
@ -64,11 +72,15 @@ public function updateBranch(Server $server, Site $site, Request $request): Redi
public function getEnv(Server $server, Site $site): RedirectResponse
{
$this->authorize('manage', $server);
return back()->with('env', $site->getEnv());
}
public function updateEnv(Server $server, Site $site, Request $request): RedirectResponse
{
$this->authorize('manage', $server);
app(UpdateEnv::class)->update($site, $request->input());
Toast::success('Env updated!');
@ -78,6 +90,8 @@ public function updateEnv(Server $server, Site $site, Request $request): Redirec
public function enableAutoDeployment(Server $server, Site $site): HtmxResponse
{
$this->authorize('manage', $server);
if (! $site->isAutoDeployment()) {
try {
$site->enableAutoDeployment();
@ -101,6 +115,8 @@ public function enableAutoDeployment(Server $server, Site $site): HtmxResponse
public function disableAutoDeployment(Server $server, Site $site): HtmxResponse
{
$this->authorize('manage', $server);
if ($site->isAutoDeployment()) {
try {
$site->disableAutoDeployment();

View File

@ -11,6 +11,8 @@ class ConsoleController extends Controller
{
public function index(Server $server): View
{
$this->authorize('manage', $server);
return view('console.index', [
'server' => $server,
]);
@ -18,6 +20,8 @@ public function index(Server $server): View
public function run(Server $server, Request $request)
{
$this->authorize('manage', $server);
$this->validate($request, [
'user' => [
'required',

View File

@ -16,6 +16,8 @@ class CronjobController extends Controller
{
public function index(Server $server): View
{
$this->authorize('manage', $server);
return view('cronjobs.index', [
'server' => $server,
'cronjobs' => $server->cronJobs,
@ -24,6 +26,8 @@ public function index(Server $server): View
public function store(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(CreateCronJob::class)->create($server, $request->input());
Toast::success('Cronjob created successfully.');
@ -33,6 +37,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function destroy(Server $server, CronJob $cronJob): RedirectResponse
{
$this->authorize('manage', $server);
app(DeleteCronJob::class)->delete($server, $cronJob);
Toast::success('Cronjob deleted successfully.');

View File

@ -18,6 +18,8 @@ class DatabaseBackupController extends Controller
{
public function show(Server $server, Backup $backup): View
{
$this->authorize('manage', $server);
return view('databases.backups', [
'server' => $server,
'databases' => $server->databases,
@ -28,6 +30,8 @@ public function show(Server $server, Backup $backup): View
public function run(Server $server, Backup $backup): RedirectResponse
{
$this->authorize('manage', $server);
app(RunBackup::class)->run($backup);
Toast::success('Backup is running.');
@ -37,6 +41,8 @@ public function run(Server $server, Backup $backup): RedirectResponse
public function store(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(CreateBackup::class)->create('database', $server, $request->input());
Toast::success('Backup created successfully.');
@ -46,6 +52,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function destroy(Server $server, Backup $backup): RedirectResponse
{
$this->authorize('manage', $server);
$backup->delete();
Toast::success('Backup deleted successfully.');
@ -55,6 +63,8 @@ public function destroy(Server $server, Backup $backup): RedirectResponse
public function restore(Server $server, Backup $backup, BackupFile $backupFile, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(RestoreBackup::class)->restore($backupFile, $request->input());
Toast::success('Backup restored successfully.');
@ -64,8 +74,17 @@ public function restore(Server $server, Backup $backup, BackupFile $backupFile,
public function destroyFile(Server $server, Backup $backup, BackupFile $backupFile): RedirectResponse
{
$this->authorize('manage', $server);
$backupFile->delete();
$backupFile
->backup
->storage
->provider()
->ssh($server)
->delete($backupFile->storagePath());
Toast::success('Backup file deleted successfully.');
return back();

View File

@ -17,6 +17,8 @@ class DatabaseController extends Controller
{
public function index(Server $server): View
{
$this->authorize('manage', $server);
return view('databases.index', [
'server' => $server,
'databases' => $server->databases,
@ -27,6 +29,8 @@ public function index(Server $server): View
public function store(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
$database = app(CreateDatabase::class)->create($server, $request->input());
if ($request->input('user')) {
@ -40,6 +44,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function destroy(Server $server, Database $database): RedirectResponse
{
$this->authorize('manage', $server);
app(DeleteDatabase::class)->delete($server, $database);
Toast::success('Database deleted successfully.');

View File

@ -16,6 +16,8 @@ class DatabaseUserController extends Controller
{
public function store(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
$database = app(CreateDatabaseUser::class)->create($server, $request->input());
if ($request->input('user')) {
@ -29,6 +31,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function destroy(Server $server, DatabaseUser $databaseUser): RedirectResponse
{
$this->authorize('manage', $server);
app(DeleteDatabaseUser::class)->delete($server, $databaseUser);
Toast::success('User deleted successfully.');
@ -38,6 +42,8 @@ public function destroy(Server $server, DatabaseUser $databaseUser): RedirectRes
public function password(Server $server, DatabaseUser $databaseUser): RedirectResponse
{
$this->authorize('manage', $server);
return back()->with([
'password' => $databaseUser->password,
]);
@ -45,6 +51,8 @@ public function password(Server $server, DatabaseUser $databaseUser): RedirectRe
public function link(Server $server, DatabaseUser $databaseUser, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(LinkUser::class)->link($databaseUser, $request->input());
Toast::success('Database linked successfully.');

View File

@ -16,6 +16,8 @@ class FirewallController extends Controller
{
public function index(Server $server): View
{
$this->authorize('manage', $server);
return view('firewall.index', [
'server' => $server,
'rules' => $server->firewallRules,
@ -24,6 +26,8 @@ public function index(Server $server): View
public function store(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(CreateRule::class)->create($server, $request->input());
Toast::success('Firewall rule created!');
@ -33,6 +37,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function destroy(Server $server, FirewallRule $firewallRule): RedirectResponse
{
$this->authorize('manage', $server);
app(DeleteRule::class)->delete($server, $firewallRule);
Toast::success('Firewall rule deleted!');

View File

@ -15,6 +15,8 @@ class MetricController extends Controller
{
public function index(Server $server, Request $request): View|RedirectResponse
{
$this->authorize('manage', $server);
$this->checkIfMonitoringServiceInstalled($server);
return view('metrics.index', [
@ -26,6 +28,8 @@ public function index(Server $server, Request $request): View|RedirectResponse
public function settings(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
$this->checkIfMonitoringServiceInstalled($server);
app(UpdateMetricSettings::class)->update($server, $request->input());
@ -37,6 +41,8 @@ public function settings(Server $server, Request $request): HtmxResponse
private function checkIfMonitoringServiceInstalled(Server $server): void
{
$this->authorize('manage', $server);
if (! $server->monitoring()) {
abort(404, 'Monitoring service is not installed on this server');
}

View File

@ -20,6 +20,8 @@ class PHPController extends Controller
{
public function index(Server $server): View
{
$this->authorize('manage', $server);
return view('php.index', [
'server' => $server,
'phps' => $server->services()->where('type', 'php')->get(),
@ -29,6 +31,8 @@ public function index(Server $server): View
public function install(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
try {
app(InstallNewPHP::class)->install($server, $request->input());
@ -42,6 +46,8 @@ public function install(Server $server, Request $request): HtmxResponse
public function installExtension(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(InstallPHPExtension::class)->install($server, $request->input());
Toast::success('PHP extension is being installed! Check the logs');
@ -51,6 +57,8 @@ public function installExtension(Server $server, Request $request): HtmxResponse
public function defaultCli(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(ChangeDefaultCli::class)->change($server, $request->input());
Toast::success('Default PHP CLI is being changed!');
@ -60,6 +68,8 @@ public function defaultCli(Server $server, Request $request): HtmxResponse
public function getIni(Server $server, Request $request): RedirectResponse
{
$this->authorize('manage', $server);
$ini = app(GetPHPIni::class)->getIni($server, $request->input());
return back()->with('ini', $ini);
@ -67,6 +77,8 @@ public function getIni(Server $server, Request $request): RedirectResponse
public function updateIni(Server $server, Request $request): RedirectResponse
{
$this->authorize('manage', $server);
app(UpdatePHPIni::class)->update($server, $request->input());
Toast::success('PHP ini updated!');
@ -78,6 +90,8 @@ public function updateIni(Server $server, Request $request): RedirectResponse
public function uninstall(Server $server, Request $request): RedirectResponse
{
$this->authorize('manage', $server);
app(UninstallPHP::class)->uninstall($server, $request->input());
Toast::success('PHP is being uninstalled!');

View File

@ -1,11 +1,10 @@
<?php
namespace App\Http\Controllers\Settings;
namespace App\Http\Controllers;
use App\Actions\User\UpdateUserPassword;
use App\Actions\User\UpdateUserProfileInformation;
use App\Facades\Toast;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@ -14,7 +13,7 @@ class ProfileController extends Controller
{
public function index(): View
{
return view('settings.profile.index');
return view('profile.index');
}
public function info(Request $request): RedirectResponse

View File

@ -19,6 +19,8 @@ class QueueController extends Controller
{
public function index(Server $server, Site $site): View
{
$this->authorize('manage', $server);
return view('queues.index', [
'server' => $server,
'site' => $site,
@ -28,6 +30,8 @@ public function index(Server $server, Site $site): View
public function store(Server $server, Site $site, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(CreateQueue::class)->create($site, $request->input());
Toast::success('Queue is being created.');
@ -37,6 +41,8 @@ public function store(Server $server, Site $site, Request $request): HtmxRespons
public function action(Server $server, Site $site, Queue $queue, string $action): HtmxResponse
{
$this->authorize('manage', $server);
app(ManageQueue::class)->{$action}($queue);
Toast::success('Queue is about to '.$action);
@ -46,6 +52,8 @@ public function action(Server $server, Site $site, Queue $queue, string $action)
public function destroy(Server $server, Site $site, Queue $queue): RedirectResponse
{
$this->authorize('manage', $server);
app(DeleteQueue::class)->delete($queue);
Toast::success('Queue is being deleted.');
@ -55,6 +63,8 @@ public function destroy(Server $server, Site $site, Queue $queue): RedirectRespo
public function logs(Server $server, Site $site, Queue $queue): RedirectResponse
{
$this->authorize('manage', $server);
return back()->with('content', app(GetQueueLogs::class)->getLogs($queue));
}
}

View File

@ -17,6 +17,8 @@ class SSHKeyController extends Controller
{
public function index(Server $server): View
{
$this->authorize('manage', $server);
return view('server-ssh-keys.index', [
'server' => $server,
'keys' => $server->sshKeys,
@ -25,6 +27,8 @@ public function index(Server $server): View
public function store(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
/** @var \App\Models\SshKey $key */
$key = app(CreateSshKey::class)->create(
$request->user(),
@ -38,6 +42,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function destroy(Server $server, SshKey $sshKey): RedirectResponse
{
$this->authorize('manage', $server);
app(DeleteKeyFromServer::class)->delete($server, $sshKey);
Toast::success('SSH Key has been deleted.');
@ -47,6 +53,8 @@ public function destroy(Server $server, SshKey $sshKey): RedirectResponse
public function deploy(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(DeployKeyToServer::class)->deploy(
$request->user(),
$server,

View File

@ -17,6 +17,8 @@ class SSLController extends Controller
{
public function index(Server $server, Site $site): View
{
$this->authorize('manage', $server);
return view('ssls.index', [
'server' => $server,
'site' => $site,
@ -26,6 +28,8 @@ public function index(Server $server, Site $site): View
public function store(Server $server, Site $site, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(CreateSSL::class)->create($site, $request->input());
Toast::success('SSL certificate is being created.');
@ -35,6 +39,8 @@ public function store(Server $server, Site $site, Request $request): HtmxRespons
public function destroy(Server $server, Site $site, Ssl $ssl): RedirectResponse
{
$this->authorize('manage', $server);
app(DeleteSSL::class)->delete($ssl);
Toast::success('SSL certificate has been deleted.');

View File

@ -4,6 +4,7 @@
use App\Models\Server;
use App\Models\Site;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@ -23,10 +24,22 @@ public function search(Request $request): JsonResponse
$query->where('name', 'like', '%'.$request->input('q').'%')
->orWhere('ip', 'like', '%'.$request->input('q').'%');
})
->whereHas('project', function (Builder $projectQuery) {
$projectQuery->whereHas('users', function (Builder $userQuery) {
$userQuery->where('user_id', auth()->user()->id);
});
})
->get();
$sites = Site::query()
->where('domain', 'like', '%'.$request->input('q').'%')
->whereHas('server', function (Builder $serverQuery) {
$serverQuery->whereHas('project', function (Builder $projectQuery) {
$projectQuery->whereHas('users', function (Builder $userQuery) {
$userQuery->where('user_id', auth()->user()->id);
});
});
})
->get();
$result = [];

View File

@ -19,6 +19,9 @@ public function index(): View
{
/** @var User $user */
$user = auth()->user();
$this->authorize('viewAny', [Server::class, $user->currentProject]);
$servers = $user->currentProject->servers()->orderByDesc('created_at')->get();
return view('servers.index', compact('servers'));
@ -26,6 +29,11 @@ public function index(): View
public function create(Request $request): View
{
/** @var User $user */
$user = auth()->user();
$this->authorize('create', [Server::class, $user->currentProject]);
$provider = $request->query('provider', old('provider', \App\Enums\ServerProvider::CUSTOM));
$serverProviders = ServerProvider::query()->where('provider', $provider)->get();
@ -40,8 +48,13 @@ public function create(Request $request): View
*/
public function store(Request $request): HtmxResponse
{
/** @var User $user */
$user = auth()->user();
$this->authorize('create', [Server::class, $user->currentProject]);
$server = app(CreateServer::class)->create(
$request->user(),
$user,
$request->input()
);
@ -52,6 +65,8 @@ public function store(Request $request): HtmxResponse
public function show(Server $server): View
{
$this->authorize('view', $server);
return view('servers.show', [
'server' => $server,
]);
@ -59,6 +74,8 @@ public function show(Server $server): View
public function delete(Server $server): RedirectResponse
{
$this->authorize('delete', $server);
$server->delete();
Toast::success('Server deleted successfully.');

View File

@ -14,6 +14,8 @@ class ServerLogController extends Controller
{
public function index(Server $server): View
{
$this->authorize('manage', $server);
return view('server-logs.index', [
'server' => $server,
'pageTitle' => __('Vito Logs'),
@ -22,6 +24,8 @@ public function index(Server $server): View
public function show(Server $server, ServerLog $serverLog): RedirectResponse
{
$this->authorize('manage', $server);
if ($server->id != $serverLog->server_id) {
abort(404);
}
@ -33,6 +37,8 @@ public function show(Server $server, ServerLog $serverLog): RedirectResponse
public function remote(Server $server): View
{
$this->authorize('manage', $server);
return view('server-logs.remote-logs', [
'server' => $server,
'remote' => true,
@ -42,6 +48,8 @@ public function remote(Server $server): View
public function store(Server $server, Request $request): \App\Helpers\HtmxResponse
{
$this->authorize('manage', $server);
app(CreateServerLog::class)->create($server, $request->input());
Toast::success('Log added successfully.');
@ -51,6 +59,8 @@ public function store(Server $server, Request $request): \App\Helpers\HtmxRespon
public function destroy(Server $server, ServerLog $serverLog): RedirectResponse
{
$this->authorize('manage', $server);
$serverLog->delete();
Toast::success('Remote log deleted successfully.');

View File

@ -15,11 +15,15 @@ class ServerSettingController extends Controller
{
public function index(Server $server): View
{
$this->authorize('manage', $server);
return view('server-settings.index', compact('server'));
}
public function checkConnection(Server $server): RedirectResponse|HtmxResponse
{
$this->authorize('manage', $server);
$oldStatus = $server->status;
$server = $server->checkConnection();
@ -41,6 +45,8 @@ public function checkConnection(Server $server): RedirectResponse|HtmxResponse
public function reboot(Server $server): HtmxResponse
{
$this->authorize('manage', $server);
app(RebootServer::class)->reboot($server);
Toast::info('Server is rebooting.');
@ -50,6 +56,8 @@ public function reboot(Server $server): HtmxResponse
public function edit(Request $request, Server $server): RedirectResponse
{
$this->authorize('manage', $server);
app(EditServer::class)->edit($server, $request->input());
Toast::success('Server updated.');

View File

@ -16,6 +16,8 @@ class ServiceController extends Controller
{
public function index(Server $server): View
{
$this->authorize('manage', $server);
return view('services.index', [
'server' => $server,
'services' => $server->services,
@ -24,6 +26,8 @@ public function index(Server $server): View
public function start(Server $server, Service $service): RedirectResponse
{
$this->authorize('manage', $server);
$service->start();
Toast::success('Service is being started!');
@ -33,6 +37,8 @@ public function start(Server $server, Service $service): RedirectResponse
public function stop(Server $server, Service $service): RedirectResponse
{
$this->authorize('manage', $server);
$service->stop();
Toast::success('Service is being stopped!');
@ -42,6 +48,8 @@ public function stop(Server $server, Service $service): RedirectResponse
public function restart(Server $server, Service $service): RedirectResponse
{
$this->authorize('manage', $server);
$service->restart();
Toast::success('Service is being restarted!');
@ -51,6 +59,8 @@ public function restart(Server $server, Service $service): RedirectResponse
public function enable(Server $server, Service $service): RedirectResponse
{
$this->authorize('manage', $server);
$service->enable();
Toast::success('Service is being enabled!');
@ -60,6 +70,8 @@ public function enable(Server $server, Service $service): RedirectResponse
public function disable(Server $server, Service $service): RedirectResponse
{
$this->authorize('manage', $server);
$service->disable();
Toast::success('Service is being disabled!');
@ -69,15 +81,19 @@ public function disable(Server $server, Service $service): RedirectResponse
public function install(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(Install::class)->install($server, $request->input());
Toast::success('Service is being uninstalled!');
Toast::success('Service is being installed!');
return htmx()->back();
}
public function uninstall(Server $server, Service $service): HtmxResponse
{
$this->authorize('manage', $server);
app(Uninstall::class)->uninstall($service);
Toast::success('Service is being uninstalled!');

View File

@ -29,7 +29,7 @@ public function add(Request $request): HtmxResponse
Toast::success('Channel added successfully');
return htmx()->redirect(route('notification-channels'));
return htmx()->redirect(route('settings.notification-channels'));
}
public function delete(int $id): RedirectResponse
@ -40,6 +40,6 @@ public function delete(int $id): RedirectResponse
Toast::success('Channel deleted successfully');
return redirect()->route('notification-channels');
return redirect()->route('settings.notification-channels');
}
}

View File

@ -20,7 +20,7 @@ class ProjectController extends Controller
public function index(): View
{
return view('settings.projects.index', [
'projects' => auth()->user()->projects,
'projects' => Project::all(),
]);
}
@ -30,7 +30,7 @@ public function create(Request $request): HtmxResponse
Toast::success('Project created.');
return htmx()->redirect(route('projects'));
return htmx()->redirect(route('settings.projects'));
}
public function update(Request $request, Project $project): HtmxResponse
@ -42,7 +42,7 @@ public function update(Request $request, Project $project): HtmxResponse
Toast::success('Project updated.');
return htmx()->redirect(route('projects'));
return htmx()->redirect(route('settings.projects'));
}
public function delete(Project $project): RedirectResponse
@ -74,6 +74,8 @@ public function switch($projectId): RedirectResponse
/** @var Project $project */
$project = $user->projects()->findOrFail($projectId);
$this->authorize('view', $project);
$user->current_project_id = $project->id;
$user->save();

View File

@ -29,7 +29,7 @@ public function add(Request $request): HtmxResponse
Toast::success('SSH Key added');
return htmx()->redirect(route('ssh-keys'));
return htmx()->redirect(route('settings.ssh-keys'));
}
public function delete(int $id): RedirectResponse
@ -40,6 +40,6 @@ public function delete(int $id): RedirectResponse
Toast::success('SSH Key deleted');
return redirect()->route('ssh-keys');
return redirect()->route('settings.ssh-keys');
}
}

View File

@ -30,7 +30,7 @@ public function connect(Request $request): HtmxResponse
Toast::success('Server provider connected.');
return htmx()->redirect(route('server-providers'));
return htmx()->redirect(route('settings.server-providers'));
}
public function delete(ServerProvider $serverProvider): RedirectResponse

View File

@ -29,7 +29,7 @@ public function connect(Request $request): HtmxResponse
Toast::success('Source control connected.');
return htmx()->redirect(route('source-controls'));
return htmx()->redirect(route('settings.source-controls'));
}
public function delete(SourceControl $sourceControl): RedirectResponse
@ -44,6 +44,6 @@ public function delete(SourceControl $sourceControl): RedirectResponse
Toast::success('Source control deleted.');
return redirect()->route('source-controls');
return redirect()->route('settings.source-controls');
}
}

View File

@ -30,7 +30,7 @@ public function connect(Request $request): HtmxResponse
Toast::success('Storage provider connected.');
return htmx()->redirect(route('storage-providers'));
return htmx()->redirect(route('settings.storage-providers'));
}
public function delete(StorageProvider $storageProvider): RedirectResponse

View File

@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Actions\User\CreateUser;
use App\Actions\User\UpdateUser;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class UserController extends Controller
{
public function index(): View
{
$users = User::query()->paginate(20);
return view('settings.users.index', compact('users'));
}
public function store(Request $request): HtmxResponse
{
$user = app(CreateUser::class)->create($request->input());
return htmx()->redirect(route('settings.users.show', $user));
}
public function show(User $user): View
{
return view('settings.users.show', [
'user' => $user,
]);
}
public function update(User $user, Request $request): RedirectResponse
{
app(UpdateUser::class)->update($user, $request->input());
Toast::success('User updated successfully');
return back();
}
public function updateProjects(User $user, Request $request): HtmxResponse
{
$this->validate($request, [
'projects.*' => [
'required',
Rule::exists('projects', 'id'),
],
]);
$user->projects()->sync($request->projects);
Toast::success('Projects updated successfully');
return htmx()->redirect(route('settings.users.show', $user));
}
public function destroy(User $user): RedirectResponse
{
if ($user->is(request()->user())) {
Toast::error('You cannot delete your own account');
return back();
}
$user->delete();
Toast::success('User deleted successfully');
return redirect()->route('settings.users.index');
}
}

View File

@ -19,6 +19,8 @@ class SiteController extends Controller
{
public function index(Server $server): View
{
$this->authorize('manage', $server);
return view('sites.index', [
'server' => $server,
'sites' => $server->sites()->orderByDesc('id')->get(),
@ -27,6 +29,8 @@ public function index(Server $server): View
public function store(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
$site = app(CreateSite::class)->create($server, $request->input());
Toast::success('Site created');
@ -36,6 +40,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function create(Server $server): View
{
$this->authorize('manage', $server);
return view('sites.create', [
'server' => $server,
'type' => old('type', request()->query('type', SiteType::LARAVEL)),
@ -45,6 +51,8 @@ public function create(Server $server): View
public function show(Server $server, Site $site, Request $request): View|RedirectResponse|HtmxResponse
{
$this->authorize('manage', $server);
if (in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) {
if ($request->hasHeader('HX-Request')) {
return htmx()->redirect(route('servers.sites.installing', [$server, $site]));
@ -61,6 +69,8 @@ public function show(Server $server, Site $site, Request $request): View|Redirec
public function installing(Server $server, Site $site, Request $request): View|RedirectResponse|HtmxResponse
{
$this->authorize('manage', $server);
if (! in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) {
if ($request->hasHeader('HX-Request')) {
return htmx()->redirect(route('servers.sites.show', [$server, $site]));
@ -77,6 +87,8 @@ public function installing(Server $server, Site $site, Request $request): View|R
public function destroy(Server $server, Site $site): RedirectResponse
{
$this->authorize('manage', $server);
app(DeleteSite::class)->delete($site);
Toast::success('Site is being deleted');

View File

@ -10,6 +10,8 @@ class SiteLogController extends Controller
{
public function index(Server $server, Site $site): View
{
$this->authorize('manage', $server);
return view('site-logs.index', [
'server' => $server,
'site' => $site,

View File

@ -18,6 +18,8 @@ class SiteSettingController extends Controller
{
public function index(Server $server, Site $site): View
{
$this->authorize('manage', $server);
return view('site-settings.index', [
'server' => $server,
'site' => $site,
@ -26,6 +28,8 @@ public function index(Server $server, Site $site): View
public function getVhost(Server $server, Site $site): RedirectResponse
{
$this->authorize('manage', $server);
/** @var Webserver $handler */
$handler = $server->webserver()->handler();
@ -34,6 +38,8 @@ public function getVhost(Server $server, Site $site): RedirectResponse
public function updateVhost(Server $server, Site $site, Request $request): RedirectResponse
{
$this->authorize('manage', $server);
$this->validate($request, [
'vhost' => 'required|string',
]);
@ -53,6 +59,8 @@ public function updateVhost(Server $server, Site $site, Request $request): Redir
public function updatePHPVersion(Server $server, Site $site, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
$this->validate($request, [
'version' => [
'required',
@ -73,6 +81,8 @@ public function updatePHPVersion(Server $server, Site $site, Request $request):
public function updateSourceControl(Server $server, Site $site, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
$site = app(UpdateSourceControl::class)->update($site, $request->input());
Toast::success('Source control updated successfully!');

View File

@ -68,5 +68,6 @@ class Kernel extends HttpKernel
'server-is-ready' => ServerIsReadyMiddleware::class,
'handle-ssh-errors' => HandleSSHErrors::class,
'select-current-project' => SelectCurrentProject::class,
'is-admin' => \App\Http\Middleware\IsAdmin::class,
];
}

View File

@ -14,17 +14,17 @@ class HandleSSHErrors
public function handle(Request $request, Closure $next)
{
$res = $next($request);
if ($res instanceof Response && $res->exception) {
if ($res->exception instanceof SSHConnectionError || $res->exception instanceof SSHCommandError) {
Toast::error($res->exception->getMessage());
// if ($res instanceof Response && $res->exception) {
// if ($res->exception instanceof SSHConnectionError || $res->exception instanceof SSHCommandError) {
// Toast::error($res->exception->getMessage());
if ($request->hasHeader('HX-Request')) {
return htmx()->back();
}
// if ($request->hasHeader('HX-Request')) {
// return htmx()->back();
// }
return back();
}
}
// return back();
// }
// }
return $res;
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use App\Enums\UserRole;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IsAdmin
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (auth()->user()->role !== UserRole::ADMIN) {
abort(403, 'You are not authorized to access this page.');
}
return $next($request);
}
}

View File

@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
@ -53,4 +54,9 @@ public function notificationChannels(): HasMany
{
return $this->hasMany(NotificationChannel::class);
}
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'user_project')->withTimestamps();
}
}

View File

@ -115,4 +115,27 @@ public static function log(Server $server, string $type, string $content, ?Site
$log->save();
$log->write($content);
}
public static function make(Server $server, string $type): ServerLog
{
return new static([
'server_id' => $server->id,
'name' => $server->id.'-'.strtotime('now').'-'.$type.'.log',
'type' => $type,
'disk' => config('core.logs_disk'),
]);
}
public function forSite(Site|int $site): ServerLog
{
if ($site instanceof Site) {
$site = $site->id;
}
if (is_int($site)) {
$this->site_id = $site;
}
return $this;
}
}

View File

@ -3,6 +3,7 @@
namespace App\Models;
use App\Exceptions\SourceControlIsNotConnected;
use App\Exceptions\SSHError;
use App\SiteTypes\SiteType;
use App\SSH\Services\Webserver\Webserver;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -271,6 +272,10 @@ public function hasFeature(string $feature): bool
public function getEnv(): string
{
return $this->server->os()->readFile($this->path.'/.env');
try {
return $this->server->os()->readFile($this->path.'/.env');
} catch (SSHError) {
return '';
}
}
}

View File

@ -2,7 +2,9 @@
namespace App\Models;
use App\Enums\UserRole;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Foundation\Auth\User as Authenticatable;
@ -30,6 +32,7 @@
* @property int $current_project_id
* @property Project $currentProject
* @property Collection<Project> $projects
* @property string $role
*/
class User extends Authenticatable
{
@ -43,6 +46,7 @@ class User extends Authenticatable
'password',
'timezone',
'current_project_id',
'role',
];
protected $hidden = [
@ -60,7 +64,9 @@ public static function boot(): void
parent::boot();
static::created(function (User $user) {
$user->createDefaultProject();
if (Project::count() === 0) {
$user->createDefaultProject();
}
});
}
@ -117,9 +123,9 @@ public function connectedSourceControls(): array
return $connectedSourceControls;
}
public function projects(): HasMany
public function projects(): BelongsToMany
{
return $this->hasMany(Project::class);
return $this->belongsToMany(Project::class, 'user_project')->withTimestamps();
}
public function currentProject(): HasOne
@ -138,9 +144,10 @@ public function createDefaultProject(): Project
if (! $project) {
$project = new Project();
$project->user_id = $this->id;
$project->name = 'Default';
$project->name = 'default';
$project->save();
$project->users()->attach($this->id);
}
$this->current_project_id = $project->id;
@ -148,4 +155,9 @@ public function createDefaultProject(): Project
return $project;
}
public function isAdmin(): bool
{
return $this->role === UserRole::ADMIN;
}
}

View File

@ -35,7 +35,7 @@ public function connect(): bool
__('Congratulations! 🎉'),
__("You've connected your Discord to :app", ['app' => config('app.name')])."\n".
__('Manage your notification channels')."\n".
route('notification-channels')
route('settings.notification-channels')
);
if (! $connect) {

View File

@ -35,7 +35,7 @@ public function connect(): bool
__('Congratulations! 🎉'),
__("You've connected your Slack to :app", ['app' => config('app.name')])."\n".
__('Manage your notification channels')."\n".
route('notification-channels')
route('settings.notification-channels')
);
if (! $connect) {

View File

@ -0,0 +1,35 @@
<?php
namespace App\Policies;
use App\Enums\UserRole;
use App\Models\Project;
use App\Models\User;
class ProjectPolicy
{
public function viewAny(User $user): bool
{
return $user->role === UserRole::ADMIN;
}
public function view(User $user, Project $project): bool
{
return $user->role === UserRole::ADMIN || $project->users->contains($user);
}
public function create(User $user): bool
{
return $user->role === UserRole::ADMIN;
}
public function update(User $user, Project $project): bool
{
return $user->role === UserRole::ADMIN;
}
public function delete(User $user, Project $project): bool
{
return $user->role === UserRole::ADMIN;
}
}

View File

@ -0,0 +1,41 @@
<?php
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
{
return $user->role === UserRole::ADMIN || $project->users->contains($user);
}
public function view(User $user, Server $server): bool
{
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
}
public function create(User $user, Project $project): bool
{
return $user->role === UserRole::ADMIN || $project->users->contains($user);
}
public function update(User $user, Server $server): bool
{
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
}
public function delete(User $user, Server $server): bool
{
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
}
public function manage(User $user, Server $server): bool
{
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
}
}

View File

@ -127,16 +127,18 @@ public function tail(string $path, int $lines): string
);
}
public function runScript(string $path, string $script, ?int $siteId = null): ServerLog
public function runScript(string $path, string $script, ?ServerLog $serverLog): ServerLog
{
$ssh = $this->server->ssh();
if ($serverLog) {
$ssh->setLog($serverLog);
}
$ssh->exec(
$this->getScript('run-script.sh', [
'path' => $path,
'script' => $script,
]),
'run-script',
$siteId
'run-script'
);
return $ssh->log;

View File

@ -159,7 +159,7 @@ public function runBackup(BackupFile $backupFile): void
);
// cleanup
$this->service->server->ssh()->exec('rm '.$backupFile->name.'.zip');
$this->service->server->ssh()->exec('rm '.$backupFile->path());
$backupFile->size = $upload['size'];
$backupFile->save();

View File

@ -5,6 +5,7 @@
use App\Models\Metric;
use App\SSH\HasScripts;
use App\SSH\Services\AbstractService;
use Closure;
use Illuminate\Support\Facades\Http;
use Illuminate\Validation\Rule;
use Ramsey\Uuid\Uuid;
@ -21,7 +22,12 @@ public function creationRules(array $input): array
{
return [
'type' => [
Rule::unique('services', 'type')->where('server_id', $this->service->server_id),
function (string $attribute, mixed $value, Closure $fail) {
$monitoringExists = $this->service->server->monitoring();
if ($monitoringExists) {
$fail('You already have a monitoring service on the server.');
}
},
],
'version' => [
'required',

View File

@ -44,4 +44,12 @@ public function download(string $src, string $dest): void
'download-from-dropbox'
);
}
/**
* @TODO Implement delete method
*/
public function delete(string $path): void
{
//
}
}

View File

@ -45,4 +45,12 @@ public function download(string $src, string $dest): void
'download-from-ftp'
);
}
/**
* @TODO Implement delete method
*/
public function delete(string $path): void
{
//
}
}

49
app/SSH/Storage/Local.php Normal file
View File

@ -0,0 +1,49 @@
<?php
namespace App\SSH\Storage;
use App\SSH\HasScripts;
class Local extends AbstractStorage
{
use HasScripts;
public function upload(string $src, string $dest): array
{
$destDir = dirname($this->storageProvider->credentials['path'].$dest);
$destFile = basename($this->storageProvider->credentials['path'].$dest);
$this->server->ssh()->exec(
$this->getScript('local/upload.sh', [
'src' => $src,
'dest_dir' => $destDir,
'dest_file' => $destFile,
]),
'upload-to-local'
);
return [
'size' => null,
];
}
public function download(string $src, string $dest): void
{
$this->server->ssh()->exec(
$this->getScript('local/download.sh', [
'src' => $this->storageProvider->credentials['path'].$src,
'dest' => $dest,
]),
'download-from-local'
);
}
public function delete(string $path): void
{
$this->server->ssh()->exec(
$this->getScript('local/delete.sh', [
'path' => $this->storageProvider->credentials['path'].$path,
]),
'delete-from-local'
);
}
}

View File

@ -7,4 +7,6 @@ interface Storage
public function upload(string $src, string $dest): array;
public function download(string $src, string $dest): void;
public function delete(string $path): void;
}

View File

@ -0,0 +1 @@
rm __path__

View File

@ -0,0 +1 @@
cp __src__ __dest__

View File

@ -0,0 +1,2 @@
mkdir -p __dest_dir__
cp __src__ __dest_dir__/__dest_file__

View File

@ -0,0 +1,38 @@
<?php
namespace App\StorageProviders;
use App\Models\Server;
use App\SSH\Storage\Storage;
class Local extends AbstractStorageProvider
{
public function validationRules(): array
{
return [
'path' => 'required',
];
}
public function credentialData(array $input): array
{
return [
'path' => $input['path'],
];
}
public function connect(): bool
{
return true;
}
public function ssh(Server $server): Storage
{
return new \App\SSH\Storage\Local($server, $this->storageProvider);
}
public function delete(array $paths): void
{
//
}
}

View File

@ -36,10 +36,13 @@ public function connect(bool $sftp = false): void
public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false): string
{
if ($log) {
$this->setLog($log, $siteId);
} else {
$this->log = null;
if (! $this->log && $log) {
$this->log = $this->server->logs()->create([
'site_id' => $siteId,
'name' => $this->server->id.'-'.strtotime('now').'-'.$log.'.log',
'type' => $log,
'disk' => config('core.logs_disk'),
]);
}
$this->commands[] = $command;

View File

@ -5,10 +5,10 @@
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class ProfileLayout extends Component
class SettingsLayout extends Component
{
public function render(): View
{
return view('layouts.profile');
return view('layouts.settings');
}
}

View File

@ -359,10 +359,12 @@
'storage_providers' => [
\App\Enums\StorageProvider::DROPBOX,
\App\Enums\StorageProvider::FTP,
\App\Enums\StorageProvider::LOCAL,
],
'storage_providers_class' => [
'dropbox' => \App\StorageProviders\Dropbox::class,
'ftp' => \App\StorageProviders\Ftp::class,
\App\Enums\StorageProvider::DROPBOX => \App\StorageProviders\Dropbox::class,
\App\Enums\StorageProvider::FTP => \App\StorageProviders\Ftp::class,
\App\Enums\StorageProvider::LOCAL => \App\StorageProviders\Local::class,
],
'ssl_types' => [

View File

@ -16,7 +16,6 @@ class ProjectFactory extends Factory
public function definition(): array
{
return [
'user_id' => $this->faker->randomNumber(),
'name' => $this->faker->name(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),

View File

@ -7,7 +7,6 @@
use App\Enums\ServerStatus;
use App\Enums\ServerType;
use App\Models\Server;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class ServerFactory extends Factory
@ -16,11 +15,7 @@ class ServerFactory extends Factory
public function definition(): array
{
/** @var User $user */
$user = User::factory()->create();
return [
'user_id' => $user->id,
'name' => $this->faker->name(),
'ssh_user' => 'vito',
'ip' => $this->faker->ipv4(),

View File

@ -2,6 +2,7 @@
namespace Database\Factories;
use App\Enums\UserRole;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
@ -18,6 +19,7 @@ public function definition(): array
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
'timezone' => 'UTC',
'role' => UserRole::ADMIN,
];
}
}

View File

@ -9,15 +9,13 @@
public function up(): void
{
Schema::table('storage_providers', function (Blueprint $table) {
$table->dropColumn('token');
$table->dropColumn('refresh_token');
$table->dropColumn('token_expires_at');
$table->dropColumn('label');
$table->dropColumn('connected');
$table->unsignedBigInteger('user_id')->after('id');
$table->string('profile')->after('user_id');
$table->longText('credentials')->nullable()->after('provider');
});
Schema::table('storage_providers', function (Blueprint $table) {
$table->dropColumn(['token', 'refresh_token', 'token_expires_at', 'label', 'connected']);
});
}
public function down(): void
@ -27,9 +25,9 @@ public function down(): void
$table->string('refresh_token')->nullable();
$table->string('token_expires_at')->nullable();
$table->string('label')->nullable();
$table->dropColumn('user_id');
$table->dropColumn('profile');
$table->dropColumn('credentials');
});
Schema::table('storage_providers', function (Blueprint $table) {
$table->dropColumn(['user_id', 'profile', 'credentials']);
});
}
};

View File

@ -0,0 +1,31 @@
<?php
use App\Enums\UserRole;
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('role')->default(UserRole::USER);
});
User::query()->update(['role' => UserRole::ADMIN]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('role');
});
}
};

View File

@ -0,0 +1,38 @@
<?php
use App\Models\Project;
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('user_project', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->unsignedBigInteger('project_id');
$table->timestamps();
});
Project::all()->each(function (Project $project) {
$project->users()->attach($project->user_id);
});
User::all()->each(function (User $user) {
$user->current_project_id = $user->projects()->first()?->id;
$user->save();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_project');
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('projects', function (Blueprint $table) {
$table->dropColumn('user_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('projects', function (Blueprint $table) {
$table->bigInteger('user_id')->nullable();
});
}
};

2232
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,6 @@
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.1.0",
"tippy.js": "^6.3.7",
"toastr": "^2.1.4",
"vite": "^4.5.3",
"apexcharts": "^3.44.2",
"flowbite-datepicker": "^1.2.6"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"resources/css/app.css": {
"file": "assets/app-f65997bb.css",
"file": "assets/app-268661bd.css",
"isEntry": true,
"src": "resources/css/app.css"
},
@ -12,7 +12,7 @@
"css": [
"assets/app-a1ae07b3.css"
],
"file": "assets/app-66009dff.js",
"file": "assets/app-01264060.js",
"isEntry": true,
"src": "resources/js/app.js"
}

View File

@ -1,74 +1,5 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58 58" style="enable-background:new 0 0 58 58;" xml:space="preserve">
<path style="fill:#556080;" d="M54.392,19.5H3.608C1.616,19.5,0,17.884,0,15.892V4.108C0,2.116,1.616,0.5,3.608,0.5h50.783
C56.384,0.5,58,2.116,58,4.108v11.783C58,17.884,56.384,19.5,54.392,19.5z"/>
<path style="fill:#424A60;" d="M54.392,38.5H3.608C1.616,38.5,0,36.884,0,34.892V23.108C0,21.116,1.616,19.5,3.608,19.5h50.783
c1.993,0,3.608,1.616,3.608,3.608v11.783C58,36.884,56.384,38.5,54.392,38.5z"/>
<path style="fill:#556080;" d="M54.392,57.5H3.608C1.616,57.5,0,55.884,0,53.892V42.108C0,40.116,1.616,38.5,3.608,38.5h50.783
c1.993,0,3.608,1.616,3.608,3.608v11.783C58,55.884,56.384,57.5,54.392,57.5z"/>
<circle style="fill:#7383BF;" cx="9.5" cy="10" r="3.5"/>
<circle style="fill:#7383BF;" cx="49" cy="8.5" r="1"/>
<circle style="fill:#7383BF;" cx="45" cy="8.5" r="1"/>
<circle style="fill:#7383BF;" cx="51" cy="11.5" r="1"/>
<circle style="fill:#7383BF;" cx="47" cy="11.5" r="1"/>
<circle style="fill:#7383BF;" cx="41" cy="8.5" r="1"/>
<circle style="fill:#7383BF;" cx="43" cy="11.5" r="1"/>
<circle style="fill:#7383BF;" cx="37" cy="8.5" r="1"/>
<circle style="fill:#7383BF;" cx="39" cy="11.5" r="1"/>
<circle style="fill:#7383BF;" cx="33" cy="8.5" r="1"/>
<circle style="fill:#7383BF;" cx="35" cy="11.5" r="1"/>
<circle style="fill:#7383BF;" cx="9.5" cy="29" r="3.5"/>
<circle style="fill:#7383BF;" cx="49" cy="27.5" r="1"/>
<circle style="fill:#7383BF;" cx="45" cy="27.5" r="1"/>
<circle style="fill:#7383BF;" cx="51" cy="30.5" r="1"/>
<circle style="fill:#7383BF;" cx="47" cy="30.5" r="1"/>
<circle style="fill:#7383BF;" cx="41" cy="27.5" r="1"/>
<circle style="fill:#7383BF;" cx="43" cy="30.5" r="1"/>
<circle style="fill:#7383BF;" cx="37" cy="27.5" r="1"/>
<circle style="fill:#7383BF;" cx="39" cy="30.5" r="1"/>
<circle style="fill:#7383BF;" cx="33" cy="27.5" r="1"/>
<circle style="fill:#7383BF;" cx="35" cy="30.5" r="1"/>
<circle style="fill:#7383BF;" cx="9.5" cy="48" r="3.5"/>
<circle style="fill:#7383BF;" cx="49" cy="46.5" r="1"/>
<circle style="fill:#7383BF;" cx="45" cy="46.5" r="1"/>
<circle style="fill:#7383BF;" cx="51" cy="49.5" r="1"/>
<circle style="fill:#7383BF;" cx="47" cy="49.5" r="1"/>
<circle style="fill:#7383BF;" cx="41" cy="46.5" r="1"/>
<circle style="fill:#7383BF;" cx="43" cy="49.5" r="1"/>
<circle style="fill:#7383BF;" cx="37" cy="46.5" r="1"/>
<circle style="fill:#7383BF;" cx="39" cy="49.5" r="1"/>
<circle style="fill:#7383BF;" cx="33" cy="46.5" r="1"/>
<circle style="fill:#7383BF;" cx="35" cy="49.5" r="1"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#4f46e5"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M21.75 17.25v-.228a4.5 4.5 0 0 0-.12-1.03l-2.268-9.64a3.375 3.375 0 0 0-3.285-2.602H7.923a3.375 3.375 0 0 0-3.285 2.602l-2.268 9.64a4.5 4.5 0 0 0-.12 1.03v.228m19.5 0a3 3 0 0 1-3 3H5.25a3 3 0 0 1-3-3m19.5 0a3 3 0 0 0-3-3H5.25a3 3 0 0 0-3 3m16.5 0h.008v.008h-.008v-.008Zm-3 0h.008v.008h-.008v-.008Z" />
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 505 B

View File

@ -1 +1,5 @@
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="envelope-open" class="svg-inline--fa fa-envelope-open fa-w-16 stroke-gray-500" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M494.586 164.516c-4.697-3.883-111.723-89.95-135.251-108.657C337.231 38.191 299.437 0 256 0c-43.205 0-80.636 37.717-103.335 55.859-24.463 19.45-131.07 105.195-135.15 108.549A48.004 48.004 0 0 0 0 201.485V464c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V201.509a48 48 0 0 0-17.414-36.993zM464 458a6 6 0 0 1-6 6H54a6 6 0 0 1-6-6V204.347c0-1.813.816-3.526 2.226-4.665 15.87-12.814 108.793-87.554 132.364-106.293C200.755 78.88 232.398 48 256 48c23.693 0 55.857 31.369 73.41 45.389 23.573 18.741 116.503 93.493 132.366 106.316a5.99 5.99 0 0 1 2.224 4.663V458zm-31.991-187.704c4.249 5.159 3.465 12.795-1.745 16.981-28.975 23.283-59.274 47.597-70.929 56.863C336.636 362.283 299.205 400 256 400c-43.452 0-81.287-38.237-103.335-55.86-11.279-8.967-41.744-33.413-70.927-56.865-5.21-4.187-5.993-11.822-1.745-16.981l15.258-18.528c4.178-5.073 11.657-5.843 16.779-1.726 28.618 23.001 58.566 47.035 70.56 56.571C200.143 320.631 232.307 352 256 352c23.602 0 55.246-30.88 73.41-45.389 11.994-9.535 41.944-33.57 70.563-56.568 5.122-4.116 12.601-3.346 16.778 1.727l15.258 18.526z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#4f46e5"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M16.5 12a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Zm0 0c0 1.657 1.007 3 2.25 3S21 13.657 21 12a9 9 0 1 0-2.636 6.364M16.5 12V8.25" />
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 331 B

View File

@ -1,129 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path style="fill:#53515E;" d="M477.486,424.797H34.514C15.452,424.797,0,409.345,0,390.283V38.488
C0,19.427,15.452,3.974,34.514,3.974h442.973C496.548,3.974,512,19.427,512,38.488v351.795
C512,409.345,496.548,424.797,477.486,424.797z"/>
<path style="fill:#474756;" d="M364.402,339.5c-4.512,0-8.169-3.657-8.169-8.169V82.374c0-4.512,3.657-8.169,8.169-8.169
s8.169,3.658,8.169,8.169v248.956C372.571,335.842,368.914,339.5,364.402,339.5z"/>
<path style="fill:#6F4BEF;" d="M477.486,3.974H34.514C15.452,3.974,0,19.427,0,38.488V86.97h509.662H512V38.488
C512,19.427,496.548,3.974,477.486,3.974z"/>
<path style="fill:#4628B2;" d="M323.799,54.227H171.836c-4.512,0-8.169-3.658-8.169-8.169s3.657-8.169,8.169-8.169h151.963
c4.512,0,8.169,3.658,8.169,8.169S328.311,54.227,323.799,54.227z"/>
<circle style="fill:#FFDC64;" cx="45.589" cy="46.06" r="10.92"/>
<circle style="fill:#86E56E;" cx="87.67" cy="46.06" r="10.92"/>
<circle style="fill:#FFDC64;" cx="127.42" cy="46.06" r="10.92"/>
<circle style="fill:#FF80BD;" cx="467.58" cy="46.06" r="10.92"/>
<path style="fill:#474756;" d="M460.791,316.909H53.767c-17.436,0-31.571,14.135-31.571,31.571v74.045
c3.827,1.463,7.977,2.272,12.318,2.272h442.973c5.327,0,10.372-1.209,14.876-3.364V348.48
C492.363,331.044,478.228,316.909,460.791,316.909z"/>
<circle style="fill:#AAA8C1;" cx="49.796" cy="130.059" r="8.169"/>
<path style="fill:#00AAF0;" d="M189.169,138.23H82.198c-4.512,0-8.169-3.658-8.169-8.169c0-4.512,3.657-8.169,8.169-8.169h106.971
c4.512,0,8.169,3.658,8.169,8.169C197.338,134.574,193.681,138.23,189.169,138.23z"/>
<g>
<path style="fill:#3C3B44;" d="M478.499,130.061h-80.425c-4.512,0-8.169-3.658-8.169-8.169c0-4.512,3.657-8.169,8.169-8.169h80.425
c4.512,0,8.169,3.658,8.169,8.169C486.669,126.404,483.012,130.061,478.499,130.061z"/>
<path style="fill:#3C3B44;" d="M478.499,165.189h-80.425c-4.512,0-8.169-3.658-8.169-8.169s3.657-8.169,8.169-8.169h80.425
c4.512,0,8.169,3.658,8.169,8.169S483.012,165.189,478.499,165.189z"/>
<path style="fill:#3C3B44;" d="M478.499,200.316h-80.425c-4.512,0-8.169-3.658-8.169-8.169s3.657-8.169,8.169-8.169h80.425
c4.512,0,8.169,3.658,8.169,8.169S483.012,200.316,478.499,200.316z"/>
<path style="fill:#3C3B44;" d="M478.499,235.445h-80.425c-4.512,0-8.169-3.658-8.169-8.169c0-4.512,3.657-8.169,8.169-8.169h80.425
c4.512,0,8.169,3.658,8.169,8.169C486.669,231.787,483.012,235.445,478.499,235.445z"/>
<path style="fill:#3C3B44;" d="M478.499,270.573h-80.425c-4.512,0-8.169-3.657-8.169-8.169s3.657-8.169,8.169-8.169h80.425
c4.512,0,8.169,3.657,8.169,8.169C486.669,266.915,483.012,270.573,478.499,270.573z"/>
<path style="fill:#3C3B44;" d="M478.499,305.7h-80.425c-4.512,0-8.169-3.657-8.169-8.169s3.657-8.169,8.169-8.169h80.425
c4.512,0,8.169,3.657,8.169,8.169S483.012,305.7,478.499,305.7z"/>
</g>
<circle style="fill:#AAA8C1;" cx="49.796" cy="164.369" r="8.169"/>
<path style="fill:#00AAF0;" d="M138.336,172.542H82.198c-4.512,0-8.169-3.658-8.169-8.169s3.657-8.169,8.169-8.169h56.138
c4.512,0,8.169,3.658,8.169,8.169S142.848,172.542,138.336,172.542z"/>
<circle style="fill:#AAA8C1;" cx="49.796" cy="265.67" r="8.169"/>
<path style="fill:#86E56E;" d="M166.711,273.84H82.198c-4.512,0-8.169-3.657-8.169-8.169s3.657-8.169,8.169-8.169h84.513
c4.512,0,8.169,3.657,8.169,8.169S171.223,273.84,166.711,273.84z"/>
<circle style="fill:#AAA8C1;" cx="49.796" cy="299.17" r="8.169"/>
<g>
<path style="fill:#00AAF0;" d="M166.711,307.334H82.198c-4.512,0-8.169-3.657-8.169-8.169s3.657-8.169,8.169-8.169h84.513
c4.512,0,8.169,3.657,8.169,8.169S171.223,307.334,166.711,307.334z"/>
<path style="fill:#00AAF0;" d="M223.638,273.84h-29.843c-4.512,0-8.169-3.657-8.169-8.169s3.657-8.169,8.169-8.169h29.843
c4.512,0,8.169,3.657,8.169,8.169S228.15,273.84,223.638,273.84z"/>
</g>
<path style="fill:#FFDC64;" d="M227.973,172.542h-56.137c-4.512,0-8.169-3.658-8.169-8.169s3.657-8.169,8.169-8.169h56.137
c4.512,0,8.169,3.658,8.169,8.169S232.485,172.542,227.973,172.542z"/>
<circle style="fill:#AAA8C1;" cx="49.796" cy="198.679" r="8.169"/>
<path style="fill:#FF80BD;" d="M193.795,206.852h-77.514c-4.512,0-8.169-3.658-8.169-8.169s3.657-8.169,8.169-8.169h77.514
c4.512,0,8.169,3.658,8.169,8.169S198.307,206.852,193.795,206.852z"/>
<circle style="fill:#AAA8C1;" cx="49.796" cy="231.359" r="8.169"/>
<path style="fill:#FFDC64;" d="M257.657,239.529H116.281c-4.512,0-8.169-3.658-8.169-8.169c0-4.512,3.657-8.169,8.169-8.169h141.376
c4.512,0,8.169,3.658,8.169,8.169C265.826,235.871,262.169,239.529,257.657,239.529z"/>
<path style="fill:#86E56E;" d="M305.487,206.852h-77.514c-4.512,0-8.169-3.658-8.169-8.169s3.657-8.169,8.169-8.169h77.514
c4.512,0,8.169,3.658,8.169,8.169S309.999,206.852,305.487,206.852z"/>
<path style="fill:#FF6B5C;" d="M440.54,508.025H74.019c-15.701,0-28.43-12.728-28.43-28.43V364.355
c0-15.701,12.728-28.43,28.43-28.43H440.54c15.701,0,28.43,12.728,28.43,28.43v115.239
C468.97,495.296,456.241,508.025,440.54,508.025z"/>
<path style="fill:#FF5450;" d="M440.54,335.927h-17.159l0,0c0,74.744-60.592,135.337-135.337,135.337H45.589v8.332
c0,15.701,12.728,28.43,28.43,28.43H440.54c15.701,0,28.43-12.728,28.43-28.43v-115.24
C468.97,348.655,456.241,335.927,440.54,335.927z"/>
<g>
<path style="fill:#FFFFFF;" d="M104.572,455.934v-70.32c0-1.698,0.737-3.007,2.215-3.931c1.476-0.922,3.247-1.384,5.315-1.384
h39.424c1.698,0,2.99,0.739,3.876,2.215c0.885,1.477,1.329,3.211,1.329,5.205c0,2.142-0.462,3.951-1.384,5.427
c-0.924,1.476-2.198,2.215-3.821,2.215h-29.678v18.715h16.611c1.623,0,2.896,0.664,3.821,1.993
c0.922,1.329,1.384,2.917,1.384,4.761c0,1.7-0.442,3.212-1.329,4.541c-0.886,1.329-2.178,1.993-3.876,1.993h-16.611v18.826h29.678
c1.623,0,2.896,0.738,3.821,2.215c0.922,1.476,1.384,3.285,1.384,5.425c0,1.993-0.444,3.729-1.329,5.206
c-0.886,1.477-2.178,2.215-3.876,2.215h-39.424c-2.068,0-3.839-0.46-5.315-1.385C105.309,458.943,104.572,457.633,104.572,455.934z
"/>
<path style="fill:#FFFFFF;" d="M164.259,455.934v-70.431c0-1.402,0.498-2.62,1.494-3.655c0.997-1.033,2.271-1.55,3.821-1.55h22.923
c18.162,0,27.242,7.9,27.242,23.698c0,11.517-4.468,19.01-13.399,22.48l13.51,24.474c0.369,0.517,0.554,1.182,0.554,1.993
c0,2.142-1.163,4.172-3.489,6.091c-2.325,1.921-4.78,2.878-7.364,2.878c-2.585,0-4.429-1.068-5.537-3.21l-15.172-29.458h-7.309
v26.689c0,1.699-0.85,3.009-2.548,3.93c-1.699,0.925-3.728,1.385-6.091,1.385c-2.364,0-4.393-0.46-6.091-1.385
C165.107,458.943,164.259,457.633,164.259,455.934z M181.534,415.957h10.962c3.249,0,5.721-0.794,7.42-2.381
c1.698-1.587,2.546-4.226,2.546-7.918c0-3.691-0.849-6.33-2.546-7.918c-1.7-1.587-4.172-2.381-7.42-2.381h-10.962V415.957z"/>
<path style="fill:#FFFFFF;" d="M228.931,455.934v-70.431c0-1.402,0.498-2.62,1.494-3.655c0.997-1.033,2.271-1.55,3.821-1.55h22.923
c18.162,0,27.242,7.9,27.242,23.698c0,11.517-4.468,19.01-13.399,22.48l13.51,24.474c0.369,0.517,0.554,1.182,0.554,1.993
c0,2.142-1.163,4.172-3.489,6.091c-2.325,1.921-4.78,2.878-7.364,2.878c-2.585,0-4.429-1.068-5.537-3.21l-15.172-29.458h-7.309
v26.689c0,1.699-0.85,3.009-2.548,3.93c-1.699,0.925-3.728,1.385-6.091,1.385c-2.364,0-4.393-0.46-6.091-1.385
C229.779,458.943,228.931,457.633,228.931,455.934z M246.206,415.957h10.962c3.249,0,5.721-0.794,7.42-2.381
c1.698-1.587,2.547-4.226,2.547-7.918c0-3.691-0.849-6.33-2.547-7.918c-1.7-1.587-4.172-2.381-7.42-2.381h-10.962V415.957z"/>
<path style="fill:#FFFFFF;" d="M292.718,435.779v-29.346c0-9.005,2.473-15.614,7.419-19.822c4.945-4.208,11.518-6.312,19.712-6.312
c8.267,0,14.875,2.104,19.822,6.312c4.945,4.208,7.419,10.817,7.419,19.822v29.346c0,9.008-2.474,15.614-7.419,19.822
c-4.947,4.208-11.555,6.312-19.822,6.312c-8.195,0-14.767-2.104-19.712-6.312C295.191,451.393,292.718,444.788,292.718,435.779z
M309.993,435.779c0,7.383,3.284,11.074,9.856,11.074c6.644,0,9.966-3.691,9.966-11.074v-29.346c0-7.381-3.322-11.073-9.966-11.073
c-6.573,0-9.856,3.692-9.856,11.073V435.779z"/>
<path style="fill:#FFFFFF;" d="M358.164,455.934v-70.431c0-1.402,0.498-2.62,1.495-3.655c0.997-1.033,2.271-1.55,3.821-1.55h22.923
c18.162,0,27.242,7.9,27.242,23.698c0,11.517-4.468,19.01-13.399,22.48l13.51,24.474c0.369,0.517,0.554,1.182,0.554,1.993
c0,2.142-1.163,4.172-3.489,6.091c-2.325,1.921-4.78,2.878-7.364,2.878c-2.585,0-4.429-1.068-5.537-3.21l-15.172-29.458h-7.309
v26.689c0,1.699-0.85,3.009-2.548,3.93c-1.699,0.925-3.728,1.385-6.091,1.385c-2.364,0-4.393-0.46-6.091-1.385
C359.012,458.943,358.164,457.633,358.164,455.934z M375.44,415.957h10.962c3.249,0,5.721-0.794,7.42-2.381
c1.698-1.587,2.547-4.226,2.547-7.918c0-3.691-0.849-6.33-2.547-7.918c-1.7-1.587-4.172-2.381-7.42-2.381H375.44V415.957z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#c546e5"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 0 0-1.883 2.542l.857 6a2.25 2.25 0 0 0 2.227 1.932H19.05a2.25 2.25 0 0 0 2.227-1.932l.857-6a2.25 2.25 0 0 0-1.883-2.542m-16.5 0V6A2.25 2.25 0 0 1 6 3.75h3.879a1.5 1.5 0 0 1 1.06.44l2.122 2.12a1.5 1.5 0 0 0 1.06.44H18A2.25 2.25 0 0 1 20.25 9v.776" />
</svg>

After

Width:  |  Height:  |  Size: 544 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#4f46e5"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
</svg>

After

Width:  |  Height:  |  Size: 565 B

View File

@ -1 +1,5 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="desktop" class="svg-inline--fa fa-desktop fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M528 0H48C21.5 0 0 21.5 0 48v320c0 26.5 21.5 48 48 48h192l-16 48h-72c-13.3 0-24 10.7-24 24s10.7 24 24 24h272c13.3 0 24-10.7 24-24s-10.7-24-24-24h-72l-16-48h192c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48zm-16 352H64V64h448v288z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#4f46e5"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" />
</svg>

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 476 B

View File

@ -1 +0,0 @@
<svg viewBox="0.22 -0.01 599.63 582.58" xmlns="http://www.w3.org/2000/svg" width="2500" height="2430"><path d="M599.85 297.43C599.85 133.16 465.63-.01 300.03-.01 134.5-.01.22 133.16.22 297.43c0 109.12 59.43 204.21 147.69 256.04l19.22-127.5c-28.48-31.45-45.96-72.91-45.96-118.52 0-98 80.1-177.47 178.86-177.47 98.83 0 178.87 79.47 178.87 177.47 0 46.02-17.69 87.77-46.58 119.21l19.14 127.23c88.67-51.7 148.39-147 148.39-256.46z" fill="#ff7a24"/><path d="M332.93 387.41l36.42 195.16H228.64l36.28-194.82c-35.87-13.61-61.51-48.03-61.51-88.66 0-52.53 42.58-95.1 95.03-95.1 52.53 0 95.03 42.57 95.03 95.1 0 40.29-25.15 74.49-60.54 88.32z" fill="#123467"/></svg>

Before

Width:  |  Height:  |  Size: 655 B

View File

@ -1,29 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -5.23 70 70" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg">
<metadata>
<rdf:RDF>
<cc:Work>
<dc:subject>
Miscellaneous
</dc:subject>
<dc:identifier>
health-monitoring
</dc:identifier>
<dc:title>
Health Monitoring
</dc:title>
<dc:format>
image/svg+xml
</dc:format>
<dc:publisher>
Amido Limited
</dc:publisher>
<dc:creator>
Richard Slater
</dc:creator>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
</cc:Work>
</rdf:RDF>
</metadata>
<path d="m -633.94123,753.25889 c -6.59942,-3.2916 -17.80605,-13.7307 -24.90952,-23.2035 l -0.38611,-0.5149 4.90905,0 c 3.3284,0 5.08031,-0.051 5.44092,-0.1594 0.95525,-0.2862 1.50799,-0.9179 2.58607,-2.9554 1.97619,-3.735 2.24879,-4.2224 2.2879,-4.0904 0.0218,0.074 0.44604,4.3009 0.94276,9.3939 0.87326,8.9538 0.91529,9.2823 1.27154,9.9368 0.62081,1.1407 1.47439,1.6301 2.85312,1.6359 1.01617,0 1.76269,-0.3415 2.41627,-1.1191 0.25355,-0.3016 1.82033,-3.2056 3.48173,-6.4532 l 3.02073,-5.9047 10.36659,-0.039 c 10.32236,-0.039 10.36894,-0.041 10.91581,-0.3356 1.1802,-0.6369 1.77594,-1.6202 1.77528,-2.9304 -6.9e-4,-1.3721 -0.67396,-2.4208 -1.91258,-2.9791 -0.5125,-0.231 -1.30161,-0.2501 -11.80218,-0.2858 -7.69785,-0.026 -11.47959,0.01 -11.97032,0.1108 -1.27206,0.264 -1.77303,0.7868 -3.0106,3.1416 l -1.08999,2.0739 -0.1043,-0.5158 c -0.0574,-0.2837 -0.47667,-4.3775 -0.9318,-9.0974 -0.45513,-4.7199 -0.88563,-8.7992 -0.95668,-9.0652 -0.36496,-1.3662 -1.62876,-2.2659 -3.16688,-2.2544 -1.04822,0.01 -1.94772,0.4395 -2.48617,1.1931 -0.17485,0.2447 -1.92936,3.5346 -3.8989,7.311 l -3.581,6.866 -5.76782,0.036 -5.76783,0.036 -0.83086,-1.6834 c -2.06318,-4.1804 -2.89449,-7.6097 -2.738,-11.2949 0.12425,-2.9261 0.69392,-5.0125 2.04328,-7.4832 1.10812,-2.029 3.06519,-4.3559 4.69277,-5.5795 1.78333,-1.3407 4.15216,-2.2461 6.64618,-2.5403 2.10735,-0.2485 4.60651,0.089 7.37391,0.9964 1.2153,0.3984 4.21499,1.9073 5.62954,2.8318 2.45012,1.6012 5.68511,4.4633 7.84072,6.9369 l 0.80955,0.929 0.94007,-1.2397 c 1.88483,-2.4857 4.78785,-5.1075 7.55221,-6.8208 5.19337,-3.2187 11.05786,-4.2791 15.6703,-2.8335 3.74959,1.1752 6.7744,3.9944 8.98105,8.3706 2.19828,4.3596 2.39398,9.8576 0.53892,15.1404 -1.06649,3.0372 -2.39805,5.6594 -4.46756,8.7979 -2.55838,3.88 -4.87538,6.6471 -9.08862,10.8542 -5.31708,5.3093 -11.00984,9.9038 -16.48777,13.3068 -1.60577,0.9976 -3.84246,2.2037 -4.0818,2.201 -0.0583,0 -0.75536,-0.325 -1.54898,-0.7208 z" fill="#00bcf2" transform="translate(667.003 -694.43)"/>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#46b3e5"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3.75 3v11.25A2.25 2.25 0 0 0 6 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0 1 18 16.5h-2.25m-7.5 0h7.5m-7.5 0-1 3m8.5-3 1 3m0 0 .5 1.5m-.5-1.5h-9.5m0 0-.5 1.5m.75-9 3-3 2.148 2.148A12.061 12.061 0 0 1 16.5 7.605" />
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 443 B

View File

@ -1,74 +1,5 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58 58" style="enable-background:new 0 0 58 58;" xml:space="preserve">
<path style="fill:#556080;" d="M54.392,19.5H3.608C1.616,19.5,0,17.884,0,15.892V4.108C0,2.116,1.616,0.5,3.608,0.5h50.783
C56.384,0.5,58,2.116,58,4.108v11.783C58,17.884,56.384,19.5,54.392,19.5z"/>
<path style="fill:#424A60;" d="M54.392,38.5H3.608C1.616,38.5,0,36.884,0,34.892V23.108C0,21.116,1.616,19.5,3.608,19.5h50.783
c1.993,0,3.608,1.616,3.608,3.608v11.783C58,36.884,56.384,38.5,54.392,38.5z"/>
<path style="fill:#556080;" d="M54.392,57.5H3.608C1.616,57.5,0,55.884,0,53.892V42.108C0,40.116,1.616,38.5,3.608,38.5h50.783
c1.993,0,3.608,1.616,3.608,3.608v11.783C58,55.884,56.384,57.5,54.392,57.5z"/>
<circle style="fill:#7383BF;" cx="9.5" cy="10" r="3.5"/>
<circle style="fill:#7383BF;" cx="49" cy="8.5" r="1"/>
<circle style="fill:#7383BF;" cx="45" cy="8.5" r="1"/>
<circle style="fill:#7383BF;" cx="51" cy="11.5" r="1"/>
<circle style="fill:#7383BF;" cx="47" cy="11.5" r="1"/>
<circle style="fill:#7383BF;" cx="41" cy="8.5" r="1"/>
<circle style="fill:#7383BF;" cx="43" cy="11.5" r="1"/>
<circle style="fill:#7383BF;" cx="37" cy="8.5" r="1"/>
<circle style="fill:#7383BF;" cx="39" cy="11.5" r="1"/>
<circle style="fill:#7383BF;" cx="33" cy="8.5" r="1"/>
<circle style="fill:#7383BF;" cx="35" cy="11.5" r="1"/>
<circle style="fill:#7383BF;" cx="9.5" cy="29" r="3.5"/>
<circle style="fill:#7383BF;" cx="49" cy="27.5" r="1"/>
<circle style="fill:#7383BF;" cx="45" cy="27.5" r="1"/>
<circle style="fill:#7383BF;" cx="51" cy="30.5" r="1"/>
<circle style="fill:#7383BF;" cx="47" cy="30.5" r="1"/>
<circle style="fill:#7383BF;" cx="41" cy="27.5" r="1"/>
<circle style="fill:#7383BF;" cx="43" cy="30.5" r="1"/>
<circle style="fill:#7383BF;" cx="37" cy="27.5" r="1"/>
<circle style="fill:#7383BF;" cx="39" cy="30.5" r="1"/>
<circle style="fill:#7383BF;" cx="33" cy="27.5" r="1"/>
<circle style="fill:#7383BF;" cx="35" cy="30.5" r="1"/>
<circle style="fill:#7383BF;" cx="9.5" cy="48" r="3.5"/>
<circle style="fill:#7383BF;" cx="49" cy="46.5" r="1"/>
<circle style="fill:#7383BF;" cx="45" cy="46.5" r="1"/>
<circle style="fill:#7383BF;" cx="51" cy="49.5" r="1"/>
<circle style="fill:#7383BF;" cx="47" cy="49.5" r="1"/>
<circle style="fill:#7383BF;" cx="41" cy="46.5" r="1"/>
<circle style="fill:#7383BF;" cx="43" cy="49.5" r="1"/>
<circle style="fill:#7383BF;" cx="37" cy="46.5" r="1"/>
<circle style="fill:#7383BF;" cx="39" cy="49.5" r="1"/>
<circle style="fill:#7383BF;" cx="33" cy="46.5" r="1"/>
<circle style="fill:#7383BF;" cx="35" cy="49.5" r="1"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#4f46e5"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M21.75 17.25v-.228a4.5 4.5 0 0 0-.12-1.03l-2.268-9.64a3.375 3.375 0 0 0-3.285-2.602H7.923a3.375 3.375 0 0 0-3.285 2.602l-2.268 9.64a4.5 4.5 0 0 0-.12 1.03v.228m19.5 0a3 3 0 0 1-3 3H5.25a3 3 0 0 1-3-3m19.5 0a3 3 0 0 0-3-3H5.25a3 3 0 0 0-3 3m16.5 0h.008v.008h-.008v-.008Zm-3 0h.008v.008h-.008v-.008Z" />
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 505 B

View File

@ -1,9 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" fill="white" fill-opacity="0.01" />
<rect x="4" y="8" width="40" height="32" rx="2" fill="#2F88FF" stroke="#000000" stroke-width="4"
stroke-linejoin="round" />
<path d="M12 18L19 24L12 30" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
<path d="M23 32H36" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#4f46e5"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>

Before

Width:  |  Height:  |  Size: 654 B

After

Width:  |  Height:  |  Size: 357 B

View File

@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 14 14" role="img" focusable="false" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg">
<path fill="olive"
d="M12.123424 3.09708C9.1750899 1.36849 7.0055019 1 7.0055019 1s-.0023.00035-.0055.00096c-.0033-.00061-.0055-.00096-.0055-.00096s-2.169706.36849-5.117921 2.09708c0 0-.390942 8.72672 5.117921 9.90292.0019-.00035.0036-.00096.0055-.001.0019.00035.0036.00096.0055.001 5.5087451-1.1762 5.1179221-9.90292 5.1179221-9.90292z" />
<path fill="#ff0"
d="M7.0058749 1.74098c.482647.10437 2.177431.53826 4.3878041 1.77816-.0096 1.61164-.285353 7.71006-4.3878041 8.73494V1.74098z" />
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#e55946"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.362 5.214A8.252 8.252 0 0 1 12 21 8.25 8.25 0 0 1 6.038 7.047 8.287 8.287 0 0 0 9 9.601a8.983 8.983 0 0 1 3.361-6.867 8.21 8.21 0 0 0 3 2.48Z" />
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 18a3.75 3.75 0 0 0 .495-7.468 5.99 5.99 0 0 0-1.925 3.547 5.975 5.975 0 0 1-2.133-1.001A3.75 3.75 0 0 0 12 18Z" />
</svg>

Before

Width:  |  Height:  |  Size: 781 B

After

Width:  |  Height:  |  Size: 540 B

View File

@ -1 +1,5 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="desktop" class="svg-inline--fa fa-desktop fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M528 0H48C21.5 0 0 21.5 0 48v320c0 26.5 21.5 48 48 48h192l-16 48h-72c-13.3 0-24 10.7-24 24s10.7 24 24 24h272c13.3 0 24-10.7 24-24s-10.7-24-24-24h-72l-16-48h192c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48zm-16 352H64V64h448v288z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#4f46e5"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" />
</svg>

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 476 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 683 B

View File

@ -1,4 +1,3 @@
@import "toastr.css";
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -50,14 +50,6 @@ document.body.addEventListener('htmx:afterSwap', (event) => {
});
});
import toastr from 'toastr';
window.toastr = toastr;
window.toastr.options = {
"debug": false,
"positionClass": "toast-bottom-right",
"preventDuplicates": true,
}
import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';
tippy('[data-tooltip]', {

View File

@ -1,15 +1,26 @@
<div>
@if ($site->deploymentScript)
<form
id="deploy"
@php
$hasDeploymentScript = (bool) $site->deploymentScript;
@endphp
<form
id="deploy"
@if ($hasDeploymentScript)
hx-post="{{ route("servers.sites.application.deploy", ["server" => $server, "site" => $site]) }}"
hx-swap="outerHTML"
hx-select="#deploy"
@else
data-tooltip="Click the Manage button to add a deployment script first"
@endif
>
<x-primary-button
class="flex items-center justify-between"
:active="true"
hx-disable
:disabled="(bool) !$hasDeploymentScript"
>
<x-primary-button class="flex items-center justify-between" :active="true" hx-disable>
{{ __("Deploy") }}
<x-heroicon name="o-play-circle" class="ml-1 h-5 w-5" />
</x-primary-button>
</form>
@endif
{{ __("Deploy") }}
<x-heroicon name="o-play-circle" class="ml-1 h-5 w-5" />
</x-primary-button>
</form>
</div>

View File

@ -1,8 +1,8 @@
@php
$deployments = $site
->deployments()
->latest()
->paginate(10);
->deployments()
->latest()
->paginate(10);
@endphp
<div x-data="">

View File

@ -2,8 +2,8 @@
@php
$class = $active
? "block flex w-full items-center justify-start px-4 py-2 text-left text-sm leading-5 text-primary-500 transition duration-150 ease-in-out hover:bg-gray-100 focus:bg-gray-100 focus:outline-none dark:hover:bg-gray-800/50 dark:focus:bg-gray-700"
: "block flex w-full items-center justify-start px-4 py-2 text-left text-sm leading-5 text-gray-700 transition duration-150 ease-in-out hover:bg-gray-100 focus:bg-gray-100 focus:outline-none dark:text-gray-300 dark:hover:bg-gray-800/50 dark:focus:bg-gray-700";
? "block flex w-full items-center justify-start px-4 py-2 text-left text-sm leading-5 text-primary-500 transition duration-150 ease-in-out hover:bg-gray-100 focus:bg-gray-100 focus:outline-none dark:hover:bg-gray-800/50 dark:focus:bg-gray-700"
: "block flex w-full items-center justify-start px-4 py-2 text-left text-sm leading-5 text-gray-700 transition duration-150 ease-in-out hover:bg-gray-100 focus:bg-gray-100 focus:outline-none dark:text-gray-300 dark:hover:bg-gray-800/50 dark:focus:bg-gray-700";
@endphp
<a {{ $attributes->merge(["class" => $class]) }}>

View File

@ -1,4 +1,11 @@
@props(["open" => false, "align" => "right", "width" => "48", "contentClasses" => "list-none divide-y divide-gray-100 rounded-md border border-gray-100 bg-white py-1 text-base shadow dark:divide-gray-600 dark:border-gray-600 dark:bg-gray-700"])
@props([
"open" => false,
"align" => "right",
"width" => "48",
"contentClasses" => "list-none divide-y divide-gray-100 rounded-md border border-gray-200 bg-white py-1 text-base dark:divide-gray-600 dark:border-gray-600 dark:bg-gray-700",
"search" => false,
"searchUrl" => "",
])
@php
switch ($align) {
@ -37,11 +44,33 @@
x-transition:leave="transition duration-75 ease-in"
x-transition:leave-start="scale-100 transform opacity-100"
x-transition:leave-end="scale-95 transform opacity-0"
class="{{ $width }} {{ $alignmentClasses }} absolute z-50 mt-2 rounded-md shadow-lg"
class="{{ $width }} {{ $alignmentClasses }} absolute z-50 mt-2 rounded-md"
style="display: none"
@click="open = false"
>
<div class="{{ $contentClasses }} rounded-md ring-1 ring-black ring-opacity-5">
<div class="{{ $contentClasses }} rounded-md">
@if ($search)
<div class="p-2">
<input
type="text"
x-ref="search"
x-model="search"
x-on:keydown.window.prevent.enter="open = false"
x-on:keydown.window.prevent.escape="open = false"
x-on:keydown.window.prevent.arrow-up="
open = true
$refs.search.focus()
"
x-on:keydown.window.prevent.arrow-down="
open = true
$refs.search.focus()
"
class="w-full rounded-md border border-gray-200 p-2"
placeholder="Search..."
/>
</div>
@endif
{{ $content }}
</div>
</div>

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 9-3 3m0 0 3 3m-3-3h7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 323 B

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 767 B

Some files were not shown because too many files have changed in this diff Show More