Compare commits

...

21 Commits
0.2.0 ... 0.6.0

Author SHA1 Message Date
10a6bb57a8 adding Projects feature (#85) 2024-01-02 19:50:49 +01:00
fd2244d382 update composer (#84)
* update composer
log viewer
code style format

* fix composer
2024-01-01 22:05:31 +01:00
551f1ce40e fix issue with php site creation 2024-01-01 21:47:05 +01:00
1ce92d9361 fix issue with php site creation 2024-01-01 21:44:49 +01:00
ec6e55e30c Update README.md 2024-01-01 19:49:51 +01:00
4cda14f4b8 deploy Wordpress sites via VitoDeploy (#83) 2024-01-01 16:20:57 +01:00
5e6d338bdc Bump phpseclib/phpseclib from 3.0.21 to 3.0.34 (#78)
Bumps [phpseclib/phpseclib](https://github.com/phpseclib/phpseclib) from 3.0.21 to 3.0.34.
- [Release notes](https://github.com/phpseclib/phpseclib/releases)
- [Changelog](https://github.com/phpseclib/phpseclib/blob/master/CHANGELOG.md)
- [Commits](https://github.com/phpseclib/phpseclib/compare/3.0.21...3.0.34)

---
updated-dependencies:
- dependency-name: phpseclib/phpseclib
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-07 22:40:24 +01:00
7312e3f515 Bump axios from 1.3.5 to 1.6.0 (#74)
Bumps [axios](https://github.com/axios/axios) from 1.3.5 to 1.6.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.3.5...v1.6.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-16 22:53:15 +01:00
b771db882b Update README.md 2023-11-05 11:00:31 +01:00
94977797cc Update README.md 2023-11-05 10:59:16 +01:00
c45872df55 Create CONTRIBUTING.md 2023-11-05 10:57:38 +01:00
16fae5334c Create SECURITY.md (#72) 2023-11-04 14:49:02 +01:00
7b8deddeca small bug fix to update source-control provider 2023-10-29 22:37:46 +01:00
1bf3c94358 add auto-deployment (#71)
add update source-control to site-settings
2023-10-29 22:20:15 +01:00
700cc5f44c Bump postcss from 8.4.21 to 8.4.31 (#67)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.21 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.21...8.4.31)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Saeed Vaziry <61919774+saeedvaziry@users.noreply.github.com>
2023-10-16 22:41:16 +02:00
9d13cc0756 enable php extension installation (#68) 2023-10-15 10:06:50 +02:00
f51d7900f0 build assets 2023-10-10 17:18:54 +02:00
4bd4b34d24 phpmyadmin (#66)
* add phpmyadmin

* add tests
2023-10-10 17:15:58 +02:00
7c5505be16 update laravel (#65)
* update laravel

* fix tests
2023-10-03 13:23:30 +02:00
7249cf9ed6 include unit tests in the workflows 2023-09-30 16:23:16 +02:00
8282d39722 add timezone edit to profile (#63) 2023-09-30 16:15:09 +02:00
136 changed files with 3613 additions and 1492 deletions

23
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,23 @@
# Contributing
Thank you for your interest in contributing! There are a couple of contribution guidelines that make it easier to apply the incoming suggestions.
If you want to contribute please start with the issues. Issues labeled with "Bug" are the higher priorities.
## Issues
1. Issues are the best place to propose a new feature.
2. If you are adding a feature that there is no issue for yet, please first open an issue and label it as "feature" and lets discuss it before you implement it.
3. Search the issues before proposing a feature to see if it is already under discussion. Referencing existing issues is a good way to increase the priority of your own.
4. We don't have an issue template yet, but the more detailed your explanation, the more quickly we'll be able to evaluate it.
5. Search for the issue that you also have. Give it a reaction (and comment, if you have something to add). We note that!
## Pull Requests
1. Open PRs represent issues that we're actively thinking about merging (at a pace we can manage). If we think a proposal needs more discussion, or that the existing code would require a lot of back-and-forth to merge, we might close it and suggest you make an issue.
2. All PRs should be made against the `main` branch. This can be changed in the future.
3. If you are making changes to the front-end layer, Please build the assets via `npm run build` and push it with the other changes.
4. Write tests for your code. Tests can be Unit or Feature.
5. Code refactors will be closed. For the architectural refactors open an issue first.
6. Use `./vendor/bin/pint` to style your code before opening a PR otherwise the actions will fail.
7. Typo fixes in documentation are welcome, but if it's at all debatable we might just close it.
## Misc
1. If you think we closed something incorrectly, feel free to (politely) tell us why! We're human and make mistakes.

View File

@ -10,9 +10,21 @@ ## Documentation
https://vitodeploy.com https://vitodeploy.com
## Feedbacks
https://features.vitodeploy.com
## Roadmap
https://https://features.vitodeploy.com/roadmap
## Contribution ## Contribution
Feel free to open a PR Please read the contribution guide [Here](/CONTRIBUTING.md)
## Security
Please read the security policy [Here](/SECURITY.md)
## Credits ## Credits

11
SECURITY.md Normal file
View File

@ -0,0 +1,11 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 0.x | :white_check_mark: |
## Reporting a Vulnerability
If you see a vulnerability, please open an issue or report it directly to me (sa.vaziry@gmail.com)

View File

@ -0,0 +1,36 @@
<?php
namespace App\Actions\Projects;
use App\Models\Project;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
class CreateProject
{
public function create(User $user, array $input): Project
{
$this->validate($user, $input);
$project = new Project([
'user_id' => $user->id,
'name' => $input['name'],
]);
$project->save();
return $project;
}
private function validate(User $user, array $input): void
{
Validator::make($input, [
'name' => [
'required',
'string',
'max:255',
'unique:projects,name,NULL,id,user_id,'.$user->id,
],
])->validate();
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Actions\Projects;
use App\Models\Project;
use App\Models\User;
use Illuminate\Validation\ValidationException;
class DeleteProject
{
public function delete(User $user, int $projectId): void
{
/** @var Project $project */
$project = $user->projects()->findOrFail($projectId);
if ($user->projects()->count() === 1) {
throw ValidationException::withMessages([
'project' => __('Cannot delete the last project.'),
]);
}
if ($user->current_project_id == $project->id) {
/** @var Project $randomProject */
$randomProject = $user->projects()->where('id', '!=', $project->id)->first();
$user->current_project_id = $randomProject->id;
$user->save();
}
$project->delete();
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Actions\Projects;
use App\Models\Project;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class UpdateProject
{
public function update(Project $project, array $input): Project
{
$this->validate($project, $input);
$project->name = $input['name'];
$project->save();
return $project;
}
private function validate(Project $project, array $input): void
{
Validator::make($input, [
'name' => [
'required',
'string',
'max:255',
Rule::unique('projects')->ignore($project->id),
],
])->validate();
}
}

View File

@ -26,6 +26,7 @@ public function create(User $creator, array $input): Server
$this->validateInputs($input); $this->validateInputs($input);
$server = new Server([ $server = new Server([
'project_id' => $creator->currentProject->id,
'user_id' => $creator->id, 'user_id' => $creator->id,
'name' => $input['name'], 'name' => $input['name'],
'ssh_user' => config('core.server_providers_default_user')[$input['provider']][$input['os']], 'ssh_user' => config('core.server_providers_default_user')[$input['provider']][$input['os']],

View File

@ -1,7 +1,8 @@
<?php <?php
namespace App\Actions\Database; namespace App\Actions\Service;
use App\Enums\ServiceStatus;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@ -18,23 +19,21 @@ public function install(Server $server, array $input): Service
$phpMyAdmin = $server->defaultService('phpmyadmin'); $phpMyAdmin = $server->defaultService('phpmyadmin');
if ($phpMyAdmin) { if ($phpMyAdmin) {
if ($phpMyAdmin->status === 'ready') {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'install' => __('Already installed'), 'allowed_ip' => __('Already installed'),
])->errorBag('installPHPMyAdmin'); ]);
}
$phpMyAdmin->delete();
} }
$phpMyAdmin = new Service([ $phpMyAdmin = new Service([
'server_id' => $server->id, 'server_id' => $server->id,
'type' => 'phpmyadmin', 'type' => 'phpmyadmin',
'type_data' => [ 'type_data' => [
'allowed_ip' => $input['allowed_ip'], 'allowed_ip' => $input['allowed_ip'],
'port' => $input['port'],
'php' => $server->defaultService('php')->version, 'php' => $server->defaultService('php')->version,
], ],
'name' => 'phpmyadmin', 'name' => 'phpmyadmin',
'version' => '5.1.2', 'version' => '5.1.2',
'status' => 'installing', 'status' => ServiceStatus::INSTALLING,
'is_default' => 1, 'is_default' => 1,
]); ]);
$phpMyAdmin->save(); $phpMyAdmin->save();
@ -50,6 +49,12 @@ private function validate(array $input): void
{ {
Validator::make($input, [ Validator::make($input, [
'allowed_ip' => 'required', 'allowed_ip' => 'required',
])->validateWithBag('installPHPMyAdmin'); 'port' => [
'required',
'numeric',
'min:1',
'max:65535',
],
])->validate();
} }
} }

View File

@ -49,11 +49,6 @@ public function create(Server $server, array $input): Site
]); ]);
} }
// detect php version
if ($site->type()->language() === 'php') {
$site->php_version = $input['php_version'];
}
// validate type // validate type
$this->validateType($site, $input); $this->validateType($site, $input);

View File

@ -0,0 +1,35 @@
<?php
namespace App\Actions\Site;
use App\Models\Site;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class UpdateSourceControl
{
/**
* @throws ValidationException
*/
public function update(Site $site, array $input): void
{
$this->validate($input);
$site->source_control_id = $input['source_control'];
$site->save();
}
/**
* @throws ValidationException
*/
protected function validate(array $input): void
{
Validator::make($input, [
'source_control' => [
'required',
Rule::exists('source_controls', 'id'),
],
])->validate();
}
}

View File

@ -19,6 +19,10 @@ public function update(User $user, array $input): void
Validator::make($input, [ Validator::make($input, [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)], 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
'timezone' => [
'required',
Rule::in(timezone_identifiers_list()),
],
])->validateWithBag('updateProfileInformation'); ])->validateWithBag('updateProfileInformation');
if ($input['email'] !== $user->email) { if ($input['email'] !== $user->email) {
@ -27,6 +31,7 @@ public function update(User $user, array $input): void
$user->forceFill([ $user->forceFill([
'name' => $input['name'], 'name' => $input['name'],
'email' => $input['email'], 'email' => $input['email'],
'timezone' => $input['timezone'],
])->save(); ])->save();
} }
} }
@ -39,6 +44,7 @@ protected function updateVerifiedUser(User $user, array $input): void
$user->forceFill([ $user->forceFill([
'name' => $input['name'], 'name' => $input['name'],
'email' => $input['email'], 'email' => $input['email'],
'timezone' => $input['timezone'],
])->save(); ])->save();
} }
} }

View File

@ -15,13 +15,13 @@ public function create(
?int $siteId = null ?int $siteId = null
): void; ): void;
public function delete(int $id, int $siteId = null): void; public function delete(int $id, ?int $siteId = null): void;
public function restart(int $id, int $siteId = null): void; public function restart(int $id, ?int $siteId = null): void;
public function stop(int $id, int $siteId = null): void; public function stop(int $id, ?int $siteId = null): void;
public function start(int $id, int $siteId = null): void; public function start(int $id, ?int $siteId = null): void;
public function getLogs(string $logPath): string; public function getLogs(string $logPath): string;
} }

View File

@ -12,7 +12,7 @@ public function credentialData(array $input): array;
public function data(array $input): array; public function data(array $input): array;
public function connect(array $credentials = null): bool; public function connect(?array $credentials = null): bool;
public function plans(): array; public function plans(): array;

View File

@ -6,6 +6,8 @@ interface SiteType
{ {
public function language(): string; public function language(): string;
public function supportedFeatures(): array;
public function createValidationRules(array $input): array; public function createValidationRules(array $input): array;
public function createFields(array $input): array; public function createFields(array $input): array;

View File

@ -6,7 +6,7 @@ interface SourceControlProvider
{ {
public function connect(): bool; public function connect(): bool;
public function getRepo(string $repo = null): mixed; public function getRepo(?string $repo = null): mixed;
public function fullRepoUrl(string $repo, string $key): string; public function fullRepoUrl(string $repo, string $key): string;

16
app/Enums/SiteFeature.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace App\Enums;
use BenSampo\Enum\Enum;
final class SiteFeature extends Enum
{
const DEPLOYMENT = 'deployment';
const ENV = 'env';
const SSL = 'ssl';
const QUEUES = 'queues';
}

View File

@ -11,6 +11,4 @@ final class SourceControl extends Enum
const GITLAB = 'gitlab'; const GITLAB = 'gitlab';
const BITBUCKET = 'bitbucket'; const BITBUCKET = 'bitbucket';
const CUSTOM = 'custom';
} }

View File

@ -7,7 +7,7 @@
class SourceControlIsNotConnected extends Exception class SourceControlIsNotConnected extends Exception
{ {
public function __construct(protected SourceControl|string $sourceControl, string $message = null) public function __construct(protected SourceControl|string|null $sourceControl, ?string $message = null)
{ {
parent::__construct($message ?? 'Source control is not connected'); parent::__construct($message ?? 'Source control is not connected');
} }

View File

@ -31,7 +31,7 @@ class SSH
protected PrivateKey $privateKey; protected PrivateKey $privateKey;
public function init(Server $server, string $asUser = null): self public function init(Server $server, ?string $asUser = null): self
{ {
$this->connection = null; $this->connection = null;
$this->log = null; $this->log = null;
@ -87,7 +87,7 @@ public function connect(bool $sftp = false): void
/** /**
* @throws Throwable * @throws Throwable
*/ */
public function exec(string|array|SSHCommand $commands, string $log = '', int $siteId = null): string public function exec(string|array|SSHCommand $commands, string $log = '', ?int $siteId = null): string
{ {
if ($log) { if ($log) {
$this->setLog($log, $siteId); $this->setLog($log, $siteId);

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\SourceControlIsNotConnected;
use App\Models\GitHook;
use Illuminate\Http\Request;
use Throwable;
class GitHookController extends Controller
{
public function __invoke(Request $request)
{
if (! $request->input('secret')) {
abort(404);
}
/** @var GitHook $gitHook */
$gitHook = GitHook::query()
->where('secret', $request->input('secret'))
->firstOrFail();
foreach ($gitHook->actions as $action) {
if ($action == 'deploy') {
try {
$gitHook->site->deploy();
} catch (SourceControlIsNotConnected) {
// TODO: send notification
} catch (Throwable $e) {
Log::error('git-hook-exception', (array) $e);
}
}
}
return response()->json([
'success' => true,
]);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers;
use App\Models\Project;
use App\Models\User;
use Illuminate\Contracts\View\View;
class ProjectController extends Controller
{
public function index(): View
{
return view('projects.index');
}
public function switch($projectId)
{
/** @var User $user */
$user = auth()->user();
/** @var Project $project */
$project = $user->projects()->findOrFail($projectId);
$user->current_project_id = $project->id;
$user->save();
return redirect()->route('servers');
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Http\Livewire\Application;
use App\Exceptions\SourceControlIsNotConnected;
use App\Models\Site;
use App\Traits\HasToast;
use App\Traits\RefreshComponentOnBroadcast;
use Illuminate\Contracts\View\View;
use Livewire\Component;
use Throwable;
class AutoDeployment extends Component
{
use HasToast;
use RefreshComponentOnBroadcast;
public Site $site;
/**
* @throws Throwable
*/
public function enable(): void
{
if (! $this->site->auto_deployment) {
try {
$this->site->enableAutoDeployment();
$this->site->refresh();
$this->toast()->success(__('Auto deployment has been enabled.'));
} catch (SourceControlIsNotConnected) {
$this->toast()->error(__('Source control is not connected. Check site\'s settings.'));
}
}
}
/**
* @throws Throwable
*/
public function disable(): void
{
if ($this->site->auto_deployment) {
try {
$this->site->disableAutoDeployment();
$this->site->refresh();
$this->toast()->success(__('Auto deployment has been disabled.'));
} catch (SourceControlIsNotConnected) {
$this->toast()->error(__('Source control is not connected. Check site\'s settings.'));
}
}
}
public function render(): View
{
return view('livewire.application.auto-deployment');
}
}

View File

@ -11,8 +11,8 @@
class Deploy extends Component class Deploy extends Component
{ {
use RefreshComponentOnBroadcast;
use HasToast; use HasToast;
use RefreshComponentOnBroadcast;
public Site $site; public Site $site;

View File

@ -28,6 +28,7 @@ public function save(): void
session()->flash('status', 'script-updated'); session()->flash('status', 'script-updated');
$this->emitTo(Deploy::class, '$refresh'); $this->emitTo(Deploy::class, '$refresh');
$this->emitTo(AutoDeployment::class, '$refresh');
} }
public function render(): View public function render(): View

View File

@ -10,8 +10,8 @@
class DeploymentsList extends Component class DeploymentsList extends Component
{ {
use RefreshComponentOnBroadcast;
use HasCustomPaginationView; use HasCustomPaginationView;
use RefreshComponentOnBroadcast;
public Site $site; public Site $site;

View File

@ -3,11 +3,13 @@
namespace App\Http\Livewire\Php; namespace App\Http\Livewire\Php;
use App\Actions\PHP\InstallNewPHP; use App\Actions\PHP\InstallNewPHP;
use App\Actions\PHP\InstallPHPExtension;
use App\Actions\PHP\UpdatePHPIni; use App\Actions\PHP\UpdatePHPIni;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use App\SSHCommands\PHP\GetPHPIniCommand; use App\SSHCommands\PHP\GetPHPIniCommand;
use App\Traits\RefreshComponentOnBroadcast; use App\Traits\RefreshComponentOnBroadcast;
use Exception;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Livewire\Component; use Livewire\Component;
use Throwable; use Throwable;
@ -24,6 +26,10 @@ class InstalledVersions extends Component
public string $ini = 'Loading php.ini'; public string $ini = 'Loading php.ini';
public ?int $extensionId = null;
public string $extension = '';
public function install(string $version): void public function install(string $version): void
{ {
app(InstallNewPHP::class)->install($this->server, [ app(InstallNewPHP::class)->install($this->server, [
@ -35,6 +41,7 @@ public function install(string $version): void
public function restart(int $id): void public function restart(int $id): void
{ {
/** @var Service $service */
$service = Service::query()->findOrFail($id); $service = Service::query()->findOrFail($id);
$service->restart(); $service->restart();
@ -43,6 +50,7 @@ public function restart(int $id): void
public function uninstall(): void public function uninstall(): void
{ {
/** @var Service $service */
$service = Service::query()->findOrFail($this->uninstallId); $service = Service::query()->findOrFail($this->uninstallId);
$service->uninstall(); $service->uninstall();
@ -56,6 +64,7 @@ public function loadIni(int $id): void
$this->iniId = $id; $this->iniId = $id;
$this->ini = 'Loading php.ini'; $this->ini = 'Loading php.ini';
/** @var Service $service */
$service = Service::query()->findOrFail($this->iniId); $service = Service::query()->findOrFail($this->iniId);
try { try {
@ -67,6 +76,7 @@ public function loadIni(int $id): void
public function saveIni(): void public function saveIni(): void
{ {
/** @var Service $service */
$service = Service::query()->findOrFail($this->iniId); $service = Service::query()->findOrFail($this->iniId);
app(UpdatePHPIni::class)->update($service, $this->all()['ini']); app(UpdatePHPIni::class)->update($service, $this->all()['ini']);
@ -76,10 +86,32 @@ public function saveIni(): void
session()->flash('status', 'ini-updated'); session()->flash('status', 'ini-updated');
} }
/**
* @throws Exception
*/
public function installExtension(): void
{
/** @var Service $service */
$service = Service::query()->findOrFail($this->extensionId);
app(InstallPHPExtension::class)->handle($service, [
'name' => $this->extension,
]);
session()->flash('status', 'started-installation');
}
public function render(): View public function render(): View
{ {
if ($this->extensionId) {
/** @var Service $php */
$php = Service::query()->findOrFail($this->extensionId);
$installedExtensions = $php->type_data['extensions'] ?? [];
}
return view('livewire.php.installed-versions', [ return view('livewire.php.installed-versions', [
'phps' => $this->server->services()->where('type', 'php')->get(), 'phps' => $this->server->services()->where('type', 'php')->get(),
'installedExtensions' => $installedExtensions ?? [],
]); ]);
} }
} }

View File

@ -15,10 +15,13 @@ class UpdateProfileInformation extends Component
public string $email; public string $email;
public string $timezone;
public function mount(): void public function mount(): void
{ {
$this->name = auth()->user()->name; $this->name = auth()->user()->name;
$this->email = auth()->user()->email; $this->email = auth()->user()->email;
$this->timezone = auth()->user()->timezone;
} }
/** /**

View File

@ -0,0 +1,37 @@
<?php
namespace App\Http\Livewire\Projects;
use App\Traits\HasToast;
use App\Traits\RefreshComponentOnBroadcast;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class CreateProject extends Component
{
use HasToast;
use RefreshComponentOnBroadcast;
public bool $open = false;
public array $inputs = [];
public function create(): void
{
app(\App\Actions\Projects\CreateProject::class)
->create(auth()->user(), $this->inputs);
$this->emitTo(ProjectsList::class, '$refresh');
$this->dispatchBrowserEvent('created', true);
}
public function render(): View
{
if (request()->query('create')) {
$this->open = true;
}
return view('livewire.projects.create-project');
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Http\Livewire\Projects;
use App\Actions\Projects\UpdateProject;
use App\Models\Project;
use App\Traits\RefreshComponentOnBroadcast;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class EditProject extends Component
{
use RefreshComponentOnBroadcast;
public Project $project;
public array $inputs = [];
public function save(): void
{
app(UpdateProject::class)->update($this->project, $this->inputs);
$this->redirect(route('projects'));
}
public function mount(): void
{
$this->inputs = [
'name' => $this->project->name,
];
}
public function render(): View
{
return view('livewire.projects.edit-project');
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Livewire\Projects;
use App\Actions\Projects\DeleteProject;
use App\Traits\HasToast;
use App\Traits\RefreshComponentOnBroadcast;
use Illuminate\Contracts\View\View;
use Illuminate\Validation\ValidationException;
use Livewire\Component;
class ProjectsList extends Component
{
use HasToast;
use RefreshComponentOnBroadcast;
protected $listeners = [
'$refresh',
];
public int $deleteId;
public function delete(): void
{
try {
app(DeleteProject::class)->delete(auth()->user(), $this->deleteId);
$this->redirect(route('projects'));
return;
} catch (ValidationException $e) {
$this->toast()->error($e->getMessage());
}
}
public function render(): View
{
return view('livewire.projects.projects-list', [
'projects' => auth()->user()->projects()->orderByDesc('id')->get(),
]);
}
}

View File

@ -11,8 +11,8 @@
class LogsList extends Component class LogsList extends Component
{ {
use RefreshComponentOnBroadcast;
use HasCustomPaginationView; use HasCustomPaginationView;
use RefreshComponentOnBroadcast;
public ?int $count = null; public ?int $count = null;

View File

@ -2,7 +2,7 @@
namespace App\Http\Livewire\Servers; namespace App\Http\Livewire\Servers;
use App\Models\Server; use App\Models\User;
use App\Traits\RefreshComponentOnBroadcast; use App\Traits\RefreshComponentOnBroadcast;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Livewire\Component; use Livewire\Component;
@ -13,8 +13,12 @@ class ServersList extends Component
public function render(): View public function render(): View
{ {
/** @var User $user */
$user = auth()->user();
$servers = $user->currentProject->servers()->orderByDesc('created_at')->get();
return view('livewire.servers.servers-list', [ return view('livewire.servers.servers-list', [
'servers' => Server::all(), 'servers' => $servers,
]); ]);
} }
} }

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Livewire\Services;
use App\Actions\Service\InstallPHPMyAdmin as InstallPHPMyAdminAction;
use App\Models\Server;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class InstallPHPMyAdmin extends Component
{
public Server $server;
public string $allowed_ip;
public string $port = '5433';
public function install(): void
{
app(InstallPHPMyAdminAction::class)->install($this->server, $this->all());
$this->dispatchBrowserEvent('started', true);
$this->emitTo(ServicesList::class, '$refresh');
}
public function render(): View
{
return view('livewire.services.install-phpmyadmin');
}
}

View File

@ -3,6 +3,7 @@
namespace App\Http\Livewire\Services; namespace App\Http\Livewire\Services;
use App\Models\Server; use App\Models\Server;
use App\Models\Service;
use App\Traits\RefreshComponentOnBroadcast; use App\Traits\RefreshComponentOnBroadcast;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Livewire\Component; use Livewire\Component;
@ -15,6 +16,7 @@ class ServicesList extends Component
public function stop(int $id): void public function stop(int $id): void
{ {
/** @var Service $service */
$service = $this->server->services()->where('id', $id)->firstOrFail(); $service = $this->server->services()->where('id', $id)->firstOrFail();
$service->stop(); $service->stop();
@ -24,6 +26,7 @@ public function stop(int $id): void
public function start(int $id): void public function start(int $id): void
{ {
/** @var Service $service */
$service = $this->server->services()->where('id', $id)->firstOrFail(); $service = $this->server->services()->where('id', $id)->firstOrFail();
$service->start(); $service->start();
@ -33,6 +36,7 @@ public function start(int $id): void
public function restart(int $id): void public function restart(int $id): void
{ {
/** @var Service $service */
$service = $this->server->services()->where('id', $id)->firstOrFail(); $service = $this->server->services()->where('id', $id)->firstOrFail();
$service->restart(); $service->restart();
@ -40,6 +44,16 @@ public function restart(int $id): void
$this->refreshComponent([]); $this->refreshComponent([]);
} }
public function uninstall(int $id): void
{
/** @var Service $service */
$service = $this->server->services()->where('id', $id)->firstOrFail();
$service->uninstall();
$this->refreshComponent([]);
}
public function render(): View public function render(): View
{ {
return view('livewire.services.services-list', [ return view('livewire.services.services-list', [

View File

@ -2,7 +2,6 @@
namespace App\Http\Livewire\Sites; namespace App\Http\Livewire\Sites;
use App\Enums\SiteType;
use App\Exceptions\SourceControlIsNotConnected; use App\Exceptions\SourceControlIsNotConnected;
use App\Models\Server; use App\Models\Server;
use App\Models\SourceControl; use App\Models\SourceControl;
@ -16,23 +15,12 @@ class CreateSite extends Component
public Server $server; public Server $server;
public string $type = SiteType::LARAVEL; public array $inputs = [
'type' => '',
public string $domain; 'web_directory' => 'public',
'source_control' => '',
public string $alias; 'php_version' => '',
];
public string $php_version = '';
public string $web_directory = 'public';
public string $source_control = '';
public string $repository;
public string $branch;
public bool $composer;
/** /**
* @throws SourceControlIsNotConnected * @throws SourceControlIsNotConnected
@ -41,7 +29,7 @@ public function create(): void
{ {
$site = app(\App\Actions\Site\CreateSite::class)->create( $site = app(\App\Actions\Site\CreateSite::class)->create(
$this->server, $this->server,
$this->all() $this->inputs
); );
$this->redirect(route('servers.sites.show', [ $this->redirect(route('servers.sites.show', [

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Livewire\Sites;
use App\Actions\Site\UpdateSourceControl;
use App\Models\Site;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class UpdateSourceControlProvider extends Component
{
public Site $site;
public $source_control = null;
public function update(): void
{
app(UpdateSourceControl::class)->update($this->site, $this->all());
$this->resetErrorBag();
session()->flash('status', 'source-control-updated');
}
public function render(): View
{
if (! $this->source_control) {
$this->source_control = $this->site->source_control_id;
}
return view('livewire.sites.update-source-control-provider');
}
}

View File

@ -10,8 +10,8 @@
class SslsList extends Component class SslsList extends Component
{ {
use RefreshComponentOnBroadcast;
use HasToast; use HasToast;
use RefreshComponentOnBroadcast;
public Site $site; public Site $site;

View File

@ -13,7 +13,7 @@ class Initialize extends InstallationJob
protected ?string $asUser; protected ?string $asUser;
public function __construct(Server $server, string $asUser = null) public function __construct(Server $server, ?string $asUser = null)
{ {
$this->server = $server->refresh(); $this->server = $server->refresh();
$this->asUser = $asUser; $this->asUser = $asUser;

View File

@ -3,6 +3,7 @@
namespace App\Jobs\Installation; namespace App\Jobs\Installation;
use App\Actions\FirewallRule\CreateRule; use App\Actions\FirewallRule\CreateRule;
use App\Enums\ServiceStatus;
use App\Jobs\Job; use App\Jobs\Job;
use App\Models\FirewallRule; use App\Models\FirewallRule;
use App\Models\Service; use App\Models\Service;
@ -32,6 +33,9 @@ public function handle(): void
$this->downloadSource(); $this->downloadSource();
$this->setUpVHost(); $this->setUpVHost();
$this->restartPHP(); $this->restartPHP();
$this->service->update([
'status' => ServiceStatus::READY,
]);
} }
/** /**
@ -41,7 +45,7 @@ private function setUpFirewall(): void
{ {
$this->firewallRule = FirewallRule::query() $this->firewallRule = FirewallRule::query()
->where('server_id', $this->service->server_id) ->where('server_id', $this->service->server_id)
->where('port', '54331') ->where('port', $this->service->type_data['port'])
->first(); ->first();
if ($this->firewallRule) { if ($this->firewallRule) {
$this->firewallRule->source = $this->service->type_data['allowed_ip']; $this->firewallRule->source = $this->service->type_data['allowed_ip'];
@ -52,7 +56,7 @@ private function setUpFirewall(): void
[ [
'type' => 'allow', 'type' => 'allow',
'protocol' => 'tcp', 'protocol' => 'tcp',
'port' => '54331', 'port' => $this->service->type_data['port'],
'source' => $this->service->type_data['allowed_ip'], 'source' => $this->service->type_data['allowed_ip'],
'mask' => '0', 'mask' => '0',
] ]
@ -78,6 +82,7 @@ private function setUpVHost(): void
{ {
$vhost = File::get(resource_path('commands/webserver/nginx/phpmyadmin-vhost.conf')); $vhost = File::get(resource_path('commands/webserver/nginx/phpmyadmin-vhost.conf'));
$vhost = Str::replace('__php_version__', $this->service->server->defaultService('php')->version, $vhost); $vhost = Str::replace('__php_version__', $this->service->server->defaultService('php')->version, $vhost);
$vhost = Str::replace('__port__', $this->service->type_data['port'], $vhost);
$this->service->server->ssh()->exec( $this->service->server->ssh()->exec(
new CreateNginxPHPMyAdminVHostCommand($vhost), new CreateNginxPHPMyAdminVHostCommand($vhost),
'create-phpmyadmin-vhost' 'create-phpmyadmin-vhost'
@ -98,6 +103,9 @@ private function restartPHP(): void
public function failed(Throwable $throwable): Throwable public function failed(Throwable $throwable): Throwable
{ {
$this->firewallRule?->removeFromServer(); $this->firewallRule?->removeFromServer();
$this->service->update([
'status' => ServiceStatus::INSTALLATION_FAILED,
]);
throw $throwable; throw $throwable;
} }
} }

View File

@ -37,7 +37,7 @@ private function removeFirewallRule(): void
/** @var ?FirewallRule $rule */ /** @var ?FirewallRule $rule */
$rule = FirewallRule::query() $rule = FirewallRule::query()
->where('server_id', $this->service->server_id) ->where('server_id', $this->service->server_id)
->where('port', '54331') ->where('port', $this->service->type_data['port'])
->first(); ->first();
$rule?->removeFromServer(); $rule?->removeFromServer();
} }

View File

@ -52,7 +52,7 @@ public function handle(): void
$this->site->id $this->site->id
); );
if (! Str::contains($result, 'Wordpress installed!')) { if (! Str::contains($result, 'Success')) {
throw new FailedToInstallWordpress($result); throw new FailedToInstallWordpress($result);
} }
} }

View File

@ -57,19 +57,19 @@ public function server(): BelongsTo
/** /**
* create database on server * create database on server
*/ */
public function createOnServer(): void public function createOnServer(string $queue = 'ssh'): void
{ {
dispatch(new CreateOnServer($this))->onConnection('ssh'); dispatch(new CreateOnServer($this))->onConnection($queue);
} }
/** /**
* delete database from server * delete database from server
*/ */
public function deleteFromServer(): void public function deleteFromServer(string $queue = 'ssh'): void
{ {
$this->status = DatabaseStatus::DELETING; $this->status = DatabaseStatus::DELETING;
$this->save(); $this->save();
dispatch(new DeleteFromServer($this))->onConnection('ssh'); dispatch(new DeleteFromServer($this))->onConnection($queue);
} }
public function backups(): HasMany public function backups(): HasMany

View File

@ -54,17 +54,17 @@ public function scopeHasDatabase(Builder $query, string $databaseName): Builder
return $query->where('databases', 'like', "%\"$databaseName\"%"); return $query->where('databases', 'like', "%\"$databaseName\"%");
} }
public function createOnServer(): void public function createOnServer(string $queue = 'ssh'): void
{ {
dispatch(new CreateOnServer($this))->onConnection('ssh'); dispatch(new CreateOnServer($this))->onConnection($queue);
} }
public function deleteFromServer(): void public function deleteFromServer(string $queue = 'ssh'): void
{ {
$this->status = DatabaseStatus::DELETING; $this->status = DatabaseStatus::DELETING;
$this->save(); $this->save();
dispatch(new DeleteFromServer($this))->onConnection('ssh'); dispatch(new DeleteFromServer($this))->onConnection($queue);
} }
public function linkNewDatabase(string $name): void public function linkNewDatabase(string $name): void
@ -79,14 +79,14 @@ public function linkNewDatabase(string $name): void
} }
} }
public function linkUser(): void public function linkUser(string $queue = 'ssh'): void
{ {
dispatch(new LinkUser($this))->onConnection('ssh'); dispatch(new LinkUser($this))->onConnection($queue);
} }
public function unlinkUser(): void public function unlinkUser(string $queue = 'ssh'): void
{ {
dispatch(new UnlinkUser($this))->onConnection('ssh'); dispatch(new UnlinkUser($this))->onConnection($queue);
} }
public function getFullUserAttribute(): string public function getFullUserAttribute(): string

View File

@ -2,9 +2,9 @@
namespace App\Models; namespace App\Models;
use App\Exceptions\FailedToDeployGitHook;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Throwable; use Throwable;
@ -22,6 +22,8 @@
*/ */
class GitHook extends AbstractModel class GitHook extends AbstractModel
{ {
use HasFactory;
protected $fillable = [ protected $fillable = [
'site_id', 'site_id',
'source_control_id', 'source_control_id',
@ -55,9 +57,6 @@ public function scopeHasEvent(Builder $query, string $event): Builder
return $query->where('events', 'like', "%\"{$event}\"%"); return $query->where('events', 'like', "%\"{$event}\"%");
} }
/**
* @throws FailedToDeployGitHook
*/
public function deployHook(): void public function deployHook(): void
{ {
$this->update( $this->update(

56
app/Models/Project.php Normal file
View File

@ -0,0 +1,56 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
* @property int $user_id
* @property string $name
* @property Carbon $created_at
* @property Carbon $updated_at
* @property User $user
* @property Collection<Server> $servers
* @property Collection<NotificationChannel> $notificationChannels
*/
class Project extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'name',
];
public static function boot(): void
{
parent::boot();
static::deleting(function (Project $project) {
$project->servers()->each(function (Server $server) {
$server->delete();
});
});
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function servers(): HasMany
{
return $this->hasMany(Server::class);
}
public function notificationChannels(): HasMany
{
return $this->hasMany(NotificationChannel::class);
}
}

View File

@ -18,6 +18,7 @@
use Illuminate\Support\Str; use Illuminate\Support\Str;
/** /**
* @property int $project_id
* @property int $user_id * @property int $user_id
* @property string $name * @property string $name
* @property string $ssh_user * @property string $ssh_user
@ -38,6 +39,7 @@
* @property int $security_updates * @property int $security_updates
* @property int $progress * @property int $progress
* @property string $progress_step * @property string $progress_step
* @property Project $project
* @property User $creator * @property User $creator
* @property ServerProvider $serverProvider * @property ServerProvider $serverProvider
* @property ServerLog[] $logs * @property ServerLog[] $logs
@ -59,6 +61,7 @@ class Server extends AbstractModel
use HasFactory; use HasFactory;
protected $fillable = [ protected $fillable = [
'project_id',
'user_id', 'user_id',
'name', 'name',
'ssh_user', 'ssh_user',
@ -82,6 +85,7 @@ class Server extends AbstractModel
]; ];
protected $casts = [ protected $casts = [
'project_id' => 'integer',
'user_id' => 'integer', 'user_id' => 'integer',
'type_data' => 'json', 'type_data' => 'json',
'port' => 'integer', 'port' => 'integer',
@ -125,6 +129,11 @@ public static function boot(): void
}); });
} }
public function project(): BelongsTo
{
return $this->belongsTo(Project::class, 'project_id');
}
public function creator(): BelongsTo public function creator(): BelongsTo
{ {
return $this->belongsTo(User::class, 'user_id'); return $this->belongsTo(User::class, 'user_id');
@ -233,7 +242,7 @@ public function install(): void
// $this->team->notify(new ServerInstallationStarted($this)); // $this->team->notify(new ServerInstallationStarted($this));
} }
public function ssh(string $user = null): \App\Helpers\SSH|SSHFake public function ssh(?string $user = null): \App\Helpers\SSH|SSHFake
{ {
return SSH::init($this, $user); return SSH::init($this, $user);
} }
@ -263,7 +272,7 @@ public function provider(): \App\Contracts\ServerProvider
return new $providerClass($this); return new $providerClass($this);
} }
public function webserver(string $version = null): ?Service public function webserver(?string $version = null): ?Service
{ {
if (! $version) { if (! $version) {
return $this->defaultService('webserver'); return $this->defaultService('webserver');
@ -272,7 +281,7 @@ public function webserver(string $version = null): ?Service
return $this->service('webserver', $version); return $this->service('webserver', $version);
} }
public function database(string $version = null): ?Service public function database(?string $version = null): ?Service
{ {
if (! $version) { if (! $version) {
return $this->defaultService('database'); return $this->defaultService('database');
@ -281,7 +290,7 @@ public function database(string $version = null): ?Service
return $this->service('database', $version); return $this->service('database', $version);
} }
public function firewall(string $version = null): ?Service public function firewall(?string $version = null): ?Service
{ {
if (! $version) { if (! $version) {
return $this->defaultService('firewall'); return $this->defaultService('firewall');
@ -290,7 +299,7 @@ public function firewall(string $version = null): ?Service
return $this->service('firewall', $version); return $this->service('firewall', $version);
} }
public function processManager(string $version = null): ?Service public function processManager(?string $version = null): ?Service
{ {
if (! $version) { if (! $version) {
return $this->defaultService('process_manager'); return $this->defaultService('process_manager');
@ -299,7 +308,7 @@ public function processManager(string $version = null): ?Service
return $this->service('process_manager', $version); return $this->service('process_manager', $version);
} }
public function php(string $version = null): ?Service public function php(?string $version = null): ?Service
{ {
if (! $version) { if (! $version) {
return $this->defaultService('php'); return $this->defaultService('php');

View File

@ -76,7 +76,7 @@ public function uninstaller(): mixed
return new $uninstaller($this); return new $uninstaller($this);
} }
public function getUnitAttribute($value): string public function getUnitAttribute($value): ?string
{ {
if ($value) { if ($value) {
return $value; return $value;

View File

@ -6,7 +6,7 @@
use App\Enums\DeploymentStatus; use App\Enums\DeploymentStatus;
use App\Enums\SiteStatus; use App\Enums\SiteStatus;
use App\Enums\SslStatus; use App\Enums\SslStatus;
use App\Exceptions\FailedToDeployGitHook; use App\Events\Broadcast;
use App\Exceptions\SourceControlIsNotConnected; use App\Exceptions\SourceControlIsNotConnected;
use App\Jobs\Site\ChangePHPVersion; use App\Jobs\Site\ChangePHPVersion;
use App\Jobs\Site\Deploy; use App\Jobs\Site\Deploy;
@ -19,7 +19,8 @@
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Throwable; use Throwable;
/** /**
@ -341,22 +342,16 @@ public function getWebDirectoryPathAttribute(): string
/** /**
* @throws SourceControlIsNotConnected * @throws SourceControlIsNotConnected
* @throws ValidationException
* @throws FailedToDeployGitHook
* @throws Throwable * @throws Throwable
*/ */
public function enableAutoDeployment(): void public function enableAutoDeployment(): void
{ {
if ($this->gitHook) { if ($this->gitHook) {
throw ValidationException::withMessages([ return;
'auto_deployment' => __('Auto deployment already enabled'),
])->errorBag('auto_deployment');
} }
if (! $this->sourceControl()) { if (! $this->sourceControl()) {
throw ValidationException::withMessages([ throw new SourceControlIsNotConnected($this->source_control);
'auto_deployment' => __('Your application does not use any source controls'),
])->errorBag('auto_deployment');
} }
try { try {
@ -364,7 +359,7 @@ public function enableAutoDeployment(): void
$gitHook = new GitHook([ $gitHook = new GitHook([
'site_id' => $this->id, 'site_id' => $this->id,
'source_control_id' => $this->sourceControl()->id, 'source_control_id' => $this->sourceControl()->id,
'secret' => generate_uid(), 'secret' => Str::uuid()->toString(),
'actions' => ['deploy'], 'actions' => ['deploy'],
'events' => ['push'], 'events' => ['push'],
]); ]);
@ -399,4 +394,49 @@ public function getSshKeyNameAttribute(): string
{ {
return str('site_'.$this->id)->toString(); return str('site_'.$this->id)->toString();
} }
public function installationFinished(): void
{
$this->update([
'status' => SiteStatus::READY,
'progress' => 100,
]);
event(
new Broadcast('install-site-finished', [
'site' => $this,
])
);
/** @todo notify */
}
/**
* @throws Throwable
*/
public function installationFailed(Throwable $e): void
{
$this->update([
'status' => SiteStatus::INSTALLATION_FAILED,
]);
event(
new Broadcast('install-site-failed', [
'site' => $this,
])
);
/** @todo notify */
Log::error('install-site-error', [
'error' => (string) $e,
]);
throw $e;
}
public function hasFeature(string $feature): bool
{
return in_array($feature, $this->type()->supportedFeatures());
}
public function isReady(): bool
{
return $this->status === SiteStatus::READY;
}
} }

View File

@ -33,7 +33,7 @@ public function provider(): SourceControlProvider
return new $providerClass($this); return new $providerClass($this);
} }
public function getRepo(string $repo = null): ?array public function getRepo(?string $repo = null): ?array
{ {
return $this->provider()->getRepo($repo); return $this->provider()->getRepo($repo);
} }

View File

@ -28,6 +28,9 @@
* @property Collection $tokens * @property Collection $tokens
* @property string $profile_photo_url * @property string $profile_photo_url
* @property string $timezone * @property string $timezone
* @property int $current_project_id
* @property Project $currentProject
* @property Collection<Project> $projects
*/ */
class User extends Authenticatable class User extends Authenticatable
{ {
@ -41,6 +44,7 @@ class User extends Authenticatable
'email', 'email',
'password', 'password',
'timezone', 'timezone',
'current_project_id',
]; ];
protected $hidden = [ protected $hidden = [
@ -53,6 +57,20 @@ class User extends Authenticatable
protected $appends = [ protected $appends = [
]; ];
public static function boot(): void
{
parent::boot();
static::created(function (User $user) {
$user->createDefaultProject();
});
}
public function servers(): HasMany
{
return $this->hasMany(Server::class);
}
public function sshKeys(): HasMany public function sshKeys(): HasMany
{ {
return $this->hasMany(SshKey::class); return $this->hasMany(SshKey::class);
@ -105,4 +123,36 @@ public function connectedSourceControls(): array
return $connectedSourceControls; return $connectedSourceControls;
} }
public function projects(): HasMany
{
return $this->hasMany(Project::class);
}
public function currentProject(): HasOne
{
return $this->HasOne(Project::class, 'id', 'current_project_id');
}
public function isMemberOfProject(Project $project): bool
{
return $project->user_id === $this->id;
}
public function createDefaultProject(): Project
{
$project = $this->projects()->first();
if (! $project) {
$project = new Project();
$project->user_id = $this->id;
$project->name = 'Default';
$project->save();
}
$this->current_project_id = $project->id;
$this->save();
return $project;
}
} }

View File

@ -63,7 +63,7 @@ public function data(array $input): array
/** /**
* @throws CouldNotConnectToProvider * @throws CouldNotConnectToProvider
*/ */
public function connect(array $credentials = null): bool public function connect(?array $credentials = null): bool
{ {
try { try {
$this->connectToEc2ClientTest($credentials); $this->connectToEc2ClientTest($credentials);

View File

@ -10,7 +10,7 @@ abstract class AbstractProvider implements ServerProvider
{ {
protected ?Server $server; protected ?Server $server;
public function __construct(Server $server = null) public function __construct(?Server $server = null)
{ {
$this->server = $server; $this->server = $server;
} }

View File

@ -42,7 +42,7 @@ public function data(array $input): array
return []; return [];
} }
public function connect(array $credentials = null): bool public function connect(?array $credentials = null): bool
{ {
return true; return true;
} }

View File

@ -58,7 +58,7 @@ public function data(array $input): array
/** /**
* @throws CouldNotConnectToProvider * @throws CouldNotConnectToProvider
*/ */
public function connect(array $credentials = null): bool public function connect(?array $credentials = null): bool
{ {
$connect = Http::withToken($credentials['token'])->get($this->apiUrl.'/account'); $connect = Http::withToken($credentials['token'])->get($this->apiUrl.'/account');
if (! $connect->ok()) { if (! $connect->ok()) {

View File

@ -45,7 +45,7 @@ public function data(array $input): array
/** /**
* @throws CouldNotConnectToProvider * @throws CouldNotConnectToProvider
*/ */
public function connect(array $credentials = null): bool public function connect(?array $credentials = null): bool
{ {
$connect = Http::withToken($credentials['token'])->get($this->apiUrl.'/servers'); $connect = Http::withToken($credentials['token'])->get($this->apiUrl.'/servers');
if (! $connect->ok()) { if (! $connect->ok()) {

View File

@ -57,7 +57,7 @@ public function data(array $input): array
/** /**
* @throws CouldNotConnectToProvider * @throws CouldNotConnectToProvider
*/ */
public function connect(array $credentials = null): bool public function connect(?array $credentials = null): bool
{ {
$connect = Http::withToken($credentials['token'])->get($this->apiUrl.'/account'); $connect = Http::withToken($credentials['token'])->get($this->apiUrl.'/account');
if (! $connect->ok()) { if (! $connect->ok()) {

View File

@ -59,7 +59,7 @@ public function data(array $input): array
/** /**
* @throws CouldNotConnectToProvider * @throws CouldNotConnectToProvider
*/ */
public function connect(array $credentials = null): bool public function connect(?array $credentials = null): bool
{ {
$connect = Http::withToken($credentials['token'])->get($this->apiUrl.'/account'); $connect = Http::withToken($credentials['token'])->get($this->apiUrl.'/account');
if (! $connect->ok()) { if (! $connect->ok()) {

View File

@ -16,7 +16,7 @@ public function __construct(Server $server)
$this->server = $server; $this->server = $server;
} }
protected function progress(int $percentage, string $step = null): Closure protected function progress(int $percentage, ?string $step = null): Closure
{ {
return function () use ($percentage, $step) { return function () use ($percentage, $step) {
$this->server->progress = $percentage; $this->server->progress = $percentage;

View File

@ -47,7 +47,7 @@ public function create(
/** /**
* @throws Throwable * @throws Throwable
*/ */
public function delete(int $id, int $siteId = null): void public function delete(int $id, ?int $siteId = null): void
{ {
$this->service->server->ssh()->exec( $this->service->server->ssh()->exec(
new DeleteWorkerCommand($id), new DeleteWorkerCommand($id),
@ -59,7 +59,7 @@ public function delete(int $id, int $siteId = null): void
/** /**
* @throws Throwable * @throws Throwable
*/ */
public function restart(int $id, int $siteId = null): void public function restart(int $id, ?int $siteId = null): void
{ {
$this->service->server->ssh()->exec( $this->service->server->ssh()->exec(
new RestartWorkerCommand($id), new RestartWorkerCommand($id),
@ -71,7 +71,7 @@ public function restart(int $id, int $siteId = null): void
/** /**
* @throws Throwable * @throws Throwable
*/ */
public function stop(int $id, int $siteId = null): void public function stop(int $id, ?int $siteId = null): void
{ {
$this->service->server->ssh()->exec( $this->service->server->ssh()->exec(
new StopWorkerCommand($id), new StopWorkerCommand($id),
@ -83,7 +83,7 @@ public function stop(int $id, int $siteId = null): void
/** /**
* @throws Throwable * @throws Throwable
*/ */
public function start(int $id, int $siteId = null): void public function start(int $id, ?int $siteId = null): void
{ {
$this->service->server->ssh()->exec( $this->service->server->ssh()->exec(
new StartWorkerCommand($id), new StartWorkerCommand($id),

View File

@ -2,14 +2,12 @@
namespace App\SiteTypes; namespace App\SiteTypes;
use App\Enums\SiteStatus; use App\Enums\SiteFeature;
use App\Events\Broadcast;
use App\Jobs\Site\CloneRepository; use App\Jobs\Site\CloneRepository;
use App\Jobs\Site\ComposerInstall; use App\Jobs\Site\ComposerInstall;
use App\Jobs\Site\CreateVHost; use App\Jobs\Site\CreateVHost;
use App\Jobs\Site\DeployKey; use App\Jobs\Site\DeployKey;
use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Throwable; use Throwable;
@ -20,12 +18,22 @@ public function language(): string
return 'php'; return 'php';
} }
public function supportedFeatures(): array
{
return [
SiteFeature::DEPLOYMENT,
SiteFeature::ENV,
SiteFeature::SSL,
SiteFeature::QUEUES,
];
}
public function createValidationRules(array $input): array public function createValidationRules(array $input): array
{ {
return [ return [
'php_version' => [ 'php_version' => [
'required', 'required',
'in:'.implode(',', $this->site->server->installedPHPVersions()), Rule::in($this->site->server->installedPHPVersions()),
], ],
'source_control' => [ 'source_control' => [
'required', 'required',
@ -53,7 +61,8 @@ public function createFields(array $input): array
public function data(array $input): array public function data(array $input): array
{ {
return [ return [
'composer' => (bool) $input['composer'], 'composer' => isset($input['composer']) && $input['composer'],
'php_version' => $input['php_version'] ?? '',
]; ];
} }
@ -76,33 +85,12 @@ function () {
} }
$chain[] = function () { $chain[] = function () {
$this->site->update([ $this->site->installationFinished();
'status' => SiteStatus::READY,
'progress' => 100,
]);
event(
new Broadcast('install-site-finished', [
'site' => $this->site,
])
);
/** @todo notify */
}; };
Bus::chain($chain) Bus::chain($chain)
->catch(function (Throwable $e) { ->catch(function (Throwable $e) {
$this->site->update([ $this->site->installationFailed($e);
'status' => SiteStatus::INSTALLATION_FAILED,
]);
event(
new Broadcast('install-site-failed', [
'site' => $this->site,
])
);
/** @todo notify */
Log::error('install-site-error', [
'error' => (string) $e,
]);
throw $e;
}) })
->onConnection('ssh-long') ->onConnection('ssh-long')
->dispatch(); ->dispatch();

View File

@ -2,13 +2,15 @@
namespace App\SiteTypes; namespace App\SiteTypes;
use App\Enums\SiteStatus; use App\Enums\SiteFeature;
use App\Events\Broadcast;
use App\Jobs\Site\CreateVHost; use App\Jobs\Site\CreateVHost;
use App\Jobs\Site\InstallWordpress; use App\Jobs\Site\InstallWordpress;
use App\Models\Database;
use App\Models\DatabaseUser;
use App\SSHCommands\Wordpress\UpdateWordpressCommand; use App\SSHCommands\Wordpress\UpdateWordpressCommand;
use Closure;
use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Log; use Illuminate\Validation\Rule;
use Throwable; use Throwable;
class Wordpress extends AbstractSiteType class Wordpress extends AbstractSiteType
@ -18,94 +20,100 @@ public function language(): string
return 'php'; return 'php';
} }
public function supportedFeatures(): array
{
return [
SiteFeature::SSL,
];
}
public function createValidationRules(array $input): array public function createValidationRules(array $input): array
{ {
return [ return [
'php_version' => [
'required',
Rule::in($this->site->server->installedPHPVersions()),
],
'title' => 'required', 'title' => 'required',
'username' => 'required', 'username' => 'required',
'password' => 'required', 'password' => 'required',
'email' => 'required|email', 'email' => 'required|email',
'database' => 'required', 'database' => [
'database_user' => 'required', 'required',
Rule::unique('databases', 'name')->where(function ($query) {
return $query->where('server_id', $this->site->server_id);
}),
function (string $attribute, mixed $value, Closure $fail) {
if (! $this->site->server->database()) {
$fail(__('Database is not installed'));
}
},
],
'database_user' => [
'required',
Rule::unique('database_users', 'username')->where(function ($query) {
return $query->where('server_id', $this->site->server_id);
}),
],
'database_password' => 'required',
]; ];
} }
public function createFields(array $input): array public function createFields(array $input): array
{ {
return [ return [
'web_directory' => $input['web_directory'] ?? '', 'web_directory' => '',
'php_version' => $input['php_version'],
]; ];
} }
public function data(array $input): array public function data(array $input): array
{ {
$data = $this->site->type_data; return [
$data['url'] = $this->site->url; 'url' => $this->site->url,
if (isset($input['title']) && $input['title']) { 'title' => $input['title'],
$data['title'] = $input['title']; 'username' => $input['username'],
} 'email' => $input['email'],
if (isset($input['username']) && $input['username']) { 'password' => $input['password'],
$data['username'] = $input['username']; 'database' => $input['database'],
} 'database_user' => $input['database_user'],
if (isset($input['email']) && $input['email']) { 'database_password' => $input['database_password'],
$data['email'] = $input['email']; ];
}
if (isset($input['password']) && $input['password']) {
$data['password'] = $input['password'];
}
if (isset($input['database']) && $input['database']) {
$data['database'] = $input['database'];
}
if (isset($input['database_user']) && $input['database_user']) {
$data['database_user'] = $input['database_user'];
}
if (isset($input['url']) && $input['url']) {
$data['url'] = $input['url'];
}
return $data;
} }
public function install(): void public function install(): void
{ {
$chain = [ $chain = [
new CreateVHost($this->site), new CreateVHost($this->site),
$this->progress(30), $this->progress(15),
function () {
/** @var Database $database */
$database = $this->site->server->databases()->create([
'name' => $this->site->type_data['database'],
]);
$database->createOnServer('sync');
/** @var DatabaseUser $databaseUser */
$databaseUser = $this->site->server->databaseUsers()->create([
'username' => $this->site->type_data['database_user'],
'password' => $this->site->type_data['database_password'],
'databases' => [$this->site->type_data['database']],
]);
$databaseUser->createOnServer('sync');
$databaseUser->unlinkUser('sync');
$databaseUser->linkUser('sync');
},
$this->progress(50),
new InstallWordpress($this->site), new InstallWordpress($this->site),
$this->progress(65), $this->progress(75),
function () { function () {
$this->site->php()?->restart(); $this->site->php()?->restart();
$this->site->installationFinished();
}, },
]; ];
$chain[] = function () {
$this->site->update([
'status' => SiteStatus::READY,
'progress' => 100,
]);
event(
new Broadcast('install-site-finished', [
'site' => $this->site,
])
);
/** @todo notify */
};
Bus::chain($chain) Bus::chain($chain)
->catch(function (Throwable $e) { ->catch(function (Throwable $e) {
$this->site->update([ $this->site->installationFailed($e);
'status' => SiteStatus::INSTALLATION_FAILED,
]);
event(
new Broadcast('install-site-failed', [
'site' => $this->site,
])
);
/** @todo notify */
Log::error('install-site-error', [
'error' => (string) $e,
]);
throw $e;
}) })
->onConnection('ssh-long') ->onConnection('ssh-long')
->dispatch(); ->dispatch();
@ -139,32 +147,13 @@ function () {
'update-wordpress', 'update-wordpress',
$this->site->id $this->site->id
); );
$this->site->update([ $this->site->installationFinished();
'status' => SiteStatus::READY,
]);
event(
new Broadcast('install-site-finished', [
'site' => $this->site,
])
);
}, },
]; ];
Bus::chain($chain) Bus::chain($chain)
->catch(function (Throwable $e) { ->catch(function (Throwable $e) {
$this->site->update([ $this->site->installationFailed($e);
'status' => SiteStatus::INSTALLATION_FAILED,
]);
event(
new Broadcast('install-site-failed', [
'site' => $this->site,
])
);
/** @todo notify */
Log::error('install-site-error', [
'error' => (string) $e,
]);
throw $e;
}) })
->onConnection('ssh') ->onConnection('ssh')
->dispatch(); ->dispatch();

View File

@ -24,7 +24,7 @@ public function connect(): bool
/** /**
* @throws Exception * @throws Exception
*/ */
public function getRepo(string $repo = null): mixed public function getRepo(?string $repo = null): mixed
{ {
$res = Http::withToken($this->sourceControl->access_token) $res = Http::withToken($this->sourceControl->access_token)
->get($this->apiUrl."/repositories/$repo"); ->get($this->apiUrl."/repositories/$repo");
@ -46,7 +46,7 @@ public function deployHook(string $repo, array $events, string $secret): array
{ {
$response = Http::withToken($this->sourceControl->access_token)->post($this->apiUrl."/repositories/$repo/hooks", [ $response = Http::withToken($this->sourceControl->access_token)->post($this->apiUrl."/repositories/$repo/hooks", [
'description' => 'deploy', 'description' => 'deploy',
'url' => url('/git-hooks?secret='.$secret), 'url' => url('/api/git-hooks?secret='.$secret),
'events' => [ 'events' => [
'repo:'.implode(',', $events), 'repo:'.implode(',', $events),
], ],

View File

@ -1,41 +0,0 @@
<?php
namespace App\SourceControlProviders;
class Custom extends AbstractSourceControlProvider
{
public function connect(): bool
{
return true;
}
public function getRepo(string $repo = null): string
{
return '';
}
public function fullRepoUrl(string $repo, string $key): string
{
return $repo;
}
public function deployHook(string $repo, array $events, string $secret): array
{
return [];
}
public function destroyHook(string $repo, string $hookId): void
{
// TODO: Implement destroyHook() method.
}
public function getLastCommit(string $repo, string $branch): ?array
{
return null;
}
public function deployKey(string $title, string $repo, string $key): void
{
// TODO: Implement deployKey() method.
}
}

View File

@ -25,7 +25,7 @@ public function connect(): bool
/** /**
* @throws Exception * @throws Exception
*/ */
public function getRepo(string $repo = null): mixed public function getRepo(?string $repo = null): mixed
{ {
if ($repo) { if ($repo) {
$url = $this->apiUrl.'/repos/'.$repo; $url = $this->apiUrl.'/repos/'.$repo;
@ -59,7 +59,7 @@ public function deployHook(string $repo, array $events, string $secret): array
'name' => 'web', 'name' => 'web',
'events' => $events, 'events' => $events,
'config' => [ 'config' => [
'url' => url('/git-hooks?secret='.$secret), 'url' => url('/api/git-hooks?secret='.$secret),
'content_type' => 'json', 'content_type' => 'json',
], ],
'active' => true, 'active' => true,

View File

@ -25,7 +25,7 @@ public function connect(): bool
/** /**
* @throws Exception * @throws Exception
*/ */
public function getRepo(string $repo = null): mixed public function getRepo(?string $repo = null): mixed
{ {
$repository = $repo ? urlencode($repo) : null; $repository = $repo ? urlencode($repo) : null;
$res = Http::withToken($this->sourceControl->access_token) $res = Http::withToken($this->sourceControl->access_token)
@ -53,7 +53,7 @@ public function deployHook(string $repo, array $events, string $secret): array
$this->getApiUrl().'/projects/'.$repository.'/hooks', $this->getApiUrl().'/projects/'.$repository.'/hooks',
[ [
'description' => 'deploy', 'description' => 'deploy',
'url' => url('/git-hooks?secret='.$secret), 'url' => url('/api/git-hooks?secret='.$secret),
'push_events' => in_array('push', $events), 'push_events' => in_array('push', $events),
'issues_events' => false, 'issues_events' => false,
'job_events' => false, 'job_events' => false,

View File

@ -15,7 +15,7 @@ class SSHFake
protected string $output = ''; protected string $output = '';
public function init(Server $server, string $asUser = null): self public function init(Server $server, ?string $asUser = null): self
{ {
return $this; return $this;
} }
@ -47,7 +47,7 @@ public function assertExecuted(array|string $commands): void
PHPUnit::assertTrue(true, $allExecuted); PHPUnit::assertTrue(true, $allExecuted);
} }
public function exec(string|array|SSHCommand $commands, string $log = '', int $siteId = null): string public function exec(string|array|SSHCommand $commands, string $log = '', ?int $siteId = null): string
{ {
if (! is_array($commands)) { if (! is_array($commands)) {
$commands = [$commands]; $commands = [$commands];

View File

@ -16,6 +16,7 @@
"laravel/tinker": "^2.8", "laravel/tinker": "^2.8",
"livewire/livewire": "^2.12", "livewire/livewire": "^2.12",
"phpseclib/phpseclib": "~3.0", "phpseclib/phpseclib": "~3.0",
"opcodesio/log-viewer": "^2.5",
"ext-ftp": "*" "ext-ftp": "*"
}, },
"require-dev": { "require-dev": {
@ -24,7 +25,6 @@
"laravel/sail": "^1.18", "laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0", "nunomaduro/collision": "^7.0",
"opcodesio/log-viewer": "^2.5",
"phpunit/phpunit": "^10.0", "phpunit/phpunit": "^10.0",
"spatie/laravel-ignition": "^2.0" "spatie/laravel-ignition": "^2.0"
}, },

2186
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -25,8 +25,8 @@
use App\ServiceHandlers\Webserver\Nginx; use App\ServiceHandlers\Webserver\Nginx;
use App\SiteTypes\Laravel; use App\SiteTypes\Laravel;
use App\SiteTypes\PHPSite; use App\SiteTypes\PHPSite;
use App\SiteTypes\Wordpress;
use App\SourceControlProviders\Bitbucket; use App\SourceControlProviders\Bitbucket;
use App\SourceControlProviders\Custom;
use App\SourceControlProviders\Github; use App\SourceControlProviders\Github;
use App\SourceControlProviders\Gitlab; use App\SourceControlProviders\Gitlab;
use App\StorageProviders\Dropbox; use App\StorageProviders\Dropbox;
@ -263,12 +263,12 @@
'site_types' => [ 'site_types' => [
\App\Enums\SiteType::PHP, \App\Enums\SiteType::PHP,
\App\Enums\SiteType::LARAVEL, \App\Enums\SiteType::LARAVEL,
// \App\Enums\SiteType::WORDPRESS, \App\Enums\SiteType::WORDPRESS,
], ],
'site_types_class' => [ 'site_types_class' => [
\App\Enums\SiteType::PHP => PHPSite::class, \App\Enums\SiteType::PHP => PHPSite::class,
\App\Enums\SiteType::LARAVEL => Laravel::class, \App\Enums\SiteType::LARAVEL => Laravel::class,
// \App\Enums\SiteType::WORDPRESS => Wordpress::class, \App\Enums\SiteType::WORDPRESS => Wordpress::class,
], ],
/* /*
@ -284,7 +284,6 @@
'github' => Github::class, 'github' => Github::class,
'gitlab' => Gitlab::class, 'gitlab' => Gitlab::class,
'bitbucket' => Bitbucket::class, 'bitbucket' => Bitbucket::class,
'custom' => Custom::class,
], ],
/* /*
@ -292,11 +291,12 @@
*/ */
'php_extensions' => [ 'php_extensions' => [
'imagick', 'imagick',
'geoip',
'exif', 'exif',
'gmagick', 'gmagick',
'gmp', 'gmp',
'intl', 'intl',
'sqlite3',
'opcache',
], ],
/* /*
@ -324,7 +324,6 @@
'https' => 443, 'https' => 443,
'mysql' => 3306, 'mysql' => 3306,
'ftp' => 21, 'ftp' => 21,
'phpmyadmin' => 54331,
'tcp' => '', 'tcp' => '',
'udp' => '', 'udp' => '',
], ],

217
config/log-viewer.php Normal file
View File

@ -0,0 +1,217 @@
<?php
use Opcodes\LogViewer\Level;
return [
/*
|--------------------------------------------------------------------------
| Log Viewer
|--------------------------------------------------------------------------
| Log Viewer can be disabled, so it's no longer accessible via browser.
|
*/
'enabled' => env('LOG_VIEWER_ENABLED', true),
/*
|--------------------------------------------------------------------------
| Log Viewer Domain
|--------------------------------------------------------------------------
| You may change the domain where Log Viewer should be active.
| If the domain is empty, all domains will be valid.
|
*/
'route_domain' => null,
/*
|--------------------------------------------------------------------------
| Log Viewer Route
|--------------------------------------------------------------------------
| Log Viewer will be available under this URL.
|
*/
'route_path' => 'log-viewer',
/*
|--------------------------------------------------------------------------
| Back to system URL
|--------------------------------------------------------------------------
| When set, displays a link to easily get back to this URL.
| Set to `null` to hide this link.
|
| Optional label to display for the above URL.
|
*/
'back_to_system_url' => config('app.url', null),
'back_to_system_label' => null, // Displayed by default: "Back to {{ app.name }}"
/*
|--------------------------------------------------------------------------
| Log Viewer time zone.
|--------------------------------------------------------------------------
| The time zone in which to display the times in the UI. Defaults to
| the application's timezone defined in config/app.php.
|
*/
'timezone' => null,
/*
|--------------------------------------------------------------------------
| Log Viewer route middleware.
|--------------------------------------------------------------------------
| Optional middleware to use when loading the initial Log Viewer page.
|
*/
'middleware' => [
'web',
\Opcodes\LogViewer\Http\Middleware\AuthorizeLogViewer::class,
'auth',
],
/*
|--------------------------------------------------------------------------
| Log Viewer API middleware.
|--------------------------------------------------------------------------
| Optional middleware to use on every API request. The same API is also
| used from within the Log Viewer user interface.
|
*/
'api_middleware' => [
\Opcodes\LogViewer\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Opcodes\LogViewer\Http\Middleware\AuthorizeLogViewer::class,
],
/*
|--------------------------------------------------------------------------
| Log Viewer Remote hosts.
|--------------------------------------------------------------------------
| Log Viewer supports viewing Laravel logs from remote hosts. They must
| be running Log Viewer as well. Below you can define the hosts you
| would like to show in this Log Viewer instance.
|
*/
'hosts' => [
'local' => [
'name' => ucfirst(env('APP_ENV', 'local')),
],
// 'staging' => [
// 'name' => 'Staging',
// 'host' => 'https://staging.example.com/log-viewer',
// 'auth' => [ // Example of HTTP Basic auth
// 'username' => 'username',
// 'password' => 'password',
// ],
// ],
//
// 'production' => [
// 'name' => 'Production',
// 'host' => 'https://example.com/log-viewer',
// 'auth' => [ // Example of Bearer token auth
// 'token' => env('LOG_VIEWER_PRODUCTION_TOKEN'),
// ],
// 'headers' => [
// 'X-Foo' => 'Bar',
// ],
// ],
],
/*
|--------------------------------------------------------------------------
| Include file patterns
|--------------------------------------------------------------------------
|
*/
'include_files' => [
'*.log',
'**/*.log',
// '/absolute/paths/supported',
],
/*
|--------------------------------------------------------------------------
| Exclude file patterns.
|--------------------------------------------------------------------------
| This will take precedence over included files.
|
*/
'exclude_files' => [
// 'my_secret.log'
],
/*
|--------------------------------------------------------------------------
| Shorter stack trace filters.
|--------------------------------------------------------------------------
| Lines containing any of these strings will be excluded from the full log.
| This setting is only active when the function is enabled via the user interface.
|
*/
'shorter_stack_trace_excludes' => [
'/vendor/symfony/',
'/vendor/laravel/framework/',
'/vendor/barryvdh/laravel-debugbar/',
],
/*
|--------------------------------------------------------------------------
| Log matching patterns
|--------------------------------------------------------------------------
| Regexes for matching log files
|
*/
'patterns' => [
'laravel' => [
'log_matching_regex' => '/^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\.?(\d{6}([\+-]\d\d:\d\d)?)?)\].*/',
/**
* This pattern, used for processing Laravel logs, returns these results:
* $matches[0] - the full log line being tested.
* $matches[1] - full timestamp between the square brackets (includes microseconds and timezone offset)
* $matches[2] - timestamp microseconds, if available
* $matches[3] - timestamp timezone offset, if available
* $matches[4] - contents between timestamp and the severity level
* $matches[5] - environment (local, production, etc)
* $matches[6] - log severity (info, debug, error, etc)
* $matches[7] - the log text, the rest of the text.
*/
'log_parsing_regex' => '/^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\.?(\d{6}([\+-]\d\d:\d\d)?)?)\](.*?(\w+)\.|.*?)('
.implode('|', array_filter(Level::caseValues()))
.')?: (.*?)( in [\/].*?:[0-9]+)?$/is',
],
],
/*
|--------------------------------------------------------------------------
| Cache driver
|--------------------------------------------------------------------------
| Cache driver to use for storing the log indices. Indices are used to speed up
| log navigation. Defaults to your application's default cache driver.
|
*/
'cache_driver' => env('LOG_VIEWER_CACHE_DRIVER', null),
/*
|--------------------------------------------------------------------------
| Chunk size when scanning log files lazily
|--------------------------------------------------------------------------
| The size in MB of files to scan before updating the progress bar when searching across all files.
|
*/
'lazy_scan_chunk_size_in_mb' => 50,
];

View File

@ -0,0 +1,29 @@
<?php
namespace Database\Factories;
use App\Models\GitHook;
use App\Models\Site;
use App\Models\SourceControl;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
class GitHookFactory extends Factory
{
protected $model = GitHook::class;
public function definition(): array
{
return [
'secret' => $this->faker->word(),
'events' => $this->faker->words(),
'actions' => $this->faker->words(),
'hook_id' => $this->faker->word(),
'hook_response' => $this->faker->words(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
'site_id' => Site::factory(),
'source_control_id' => SourceControl::factory(),
];
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Database\Factories;
use App\Models\Project;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
/**
* @extends Factory<Project>
*/
class ProjectFactory extends Factory
{
protected $model = Project::class;
public function definition(): array
{
return [
'user_id' => $this->faker->randomNumber(),
'name' => $this->faker->name(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
}
}

View File

@ -13,7 +13,6 @@ class SourceControlFactory extends Factory
public function definition(): array public function definition(): array
{ {
return [ return [
'provider' => $this->faker->randomElement(\App\Enums\SourceControl::getValues()),
'access_token' => Str::random(10), 'access_token' => Str::random(10),
]; ];
} }
@ -44,13 +43,4 @@ public function bitbucket(): Factory
]; ];
}); });
} }
public function custom(): Factory
{
return $this->state(function (array $attributes) {
return [
'provider' => \App\Enums\SourceControl::CUSTOM,
];
});
}
} }

View File

@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('projects', function (Blueprint $table) {
$table->id();
$table->bigInteger('user_id');
$table->string('name');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('projects');
}
};

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->unsignedBigInteger('project_id')->nullable()->after('id');
});
}
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('project_id');
});
}
};

View File

@ -0,0 +1,27 @@
<?php
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->unsignedBigInteger('current_project_id')->nullable()->after('timezone');
});
User::query()->each(function (User $user) {
$project = $user->createDefaultProject();
$user->servers()->update(['project_id' => $project->id]);
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('current_project_id');
});
}
};

View File

@ -22,6 +22,7 @@ public function run(): void
]); ]);
$server = Server::factory()->create([ $server = Server::factory()->create([
'user_id' => $user->id, 'user_id' => $user->id,
'project_id' => $user->currentProject->id,
]); ]);
$server->services()->create([ $server->services()->create([
'type' => 'database', 'type' => 'database',

36
package-lock.json generated
View File

@ -10,10 +10,10 @@
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"alpinejs": "^3.4.2", "alpinejs": "^3.4.2",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"axios": "^1.1.2", "axios": "^1.6.0",
"laravel-echo": "^1.15.0", "laravel-echo": "^1.15.0",
"laravel-vite-plugin": "^0.7.2", "laravel-vite-plugin": "^0.7.2",
"postcss": "^8.4.6", "postcss": "^8.4.31",
"pusher-js": "^4.3.1", "pusher-js": "^4.3.1",
"tailwindcss": "^3.1.0", "tailwindcss": "^3.1.0",
"toastr": "^2.1.4", "toastr": "^2.1.4",
@ -596,9 +596,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.3.5", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
"integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==", "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.0", "follow-redirects": "^1.15.0",
@ -1380,9 +1380,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.21", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -1392,10 +1392,14 @@
{ {
"type": "tidelift", "type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss" "url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
} }
], ],
"dependencies": { "dependencies": {
"nanoid": "^3.3.4", "nanoid": "^3.3.6",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
}, },
@ -2284,9 +2288,9 @@
} }
}, },
"axios": { "axios": {
"version": "1.3.5", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
"integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==", "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"dev": true, "dev": true,
"requires": { "requires": {
"follow-redirects": "^1.15.0", "follow-redirects": "^1.15.0",
@ -2852,12 +2856,12 @@
"dev": true "dev": true
}, },
"postcss": { "postcss": {
"version": "8.4.21", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"nanoid": "^3.3.4", "nanoid": "^3.3.6",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }

View File

@ -12,10 +12,10 @@
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"alpinejs": "^3.4.2", "alpinejs": "^3.4.2",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"axios": "^1.1.2", "axios": "^1.6.0",
"laravel-echo": "^1.15.0", "laravel-echo": "^1.15.0",
"laravel-vite-plugin": "^0.7.2", "laravel-vite-plugin": "^0.7.2",
"postcss": "^8.4.6", "postcss": "^8.4.31",
"pusher-js": "^4.3.1", "pusher-js": "^4.3.1",
"tailwindcss": "^3.1.0", "tailwindcss": "^3.1.0",
"toastr": "^2.1.4", "toastr": "^2.1.4",

View File

@ -4,6 +4,9 @@
<testsuite name="Feature"> <testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory> <directory suffix="Test.php">./tests/Feature</directory>
</testsuite> </testsuite>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
</testsuites> </testsuites>
<coverage/> <coverage/>
<php> <php>

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,11 +1,11 @@
{ {
"resources/css/app.css": { "resources/css/app.css": {
"file": "assets/app-99c9ce18.css", "file": "assets/app-f482c864.css",
"isEntry": true, "isEntry": true,
"src": "resources/css/app.css" "src": "resources/css/app.css"
}, },
"resources/js/app.js": { "resources/js/app.js": {
"file": "assets/app-fa1f93fa.js", "file": "assets/app-9aa488bb.js",
"isEntry": true, "isEntry": true,
"src": "resources/js/app.js" "src": "resources/js/app.js"
} }

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg 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:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="3890" height="2168" id="svg2">
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<defs id="defs6"/>
<g id="g5" style="fill:#cccccc">
<path d="m 2889.39,6.3480122 -2.04,-4.07 c -1.01,-1.01 -2.03,-2.04 -4.06,-2.04 l -4.08,1.03 c -2.03,2.03 -3.05,3.05 -3.05,5.08 L 2789.64,1572.695 l 13.24,-2.035 83.46,-1523.70199 99.75,163.88 -1.02,0 c 75.32,221.88 106.87,458.02 94.66,708.41003 l 6.11,9.16 98.73,175.05996 6.1,6.11 c 151.66,133.336 321.63,222.907 509.94,268.707 l 45.8,74.309 6.11,2.031 1.02,-2.031 -866.19,-1416.82599 2.04,-29.52" id="path14"/>
<path d="m 2858.86,559.02801 c 106.87,218.84 145.54,549.62999 117.05,992.39199 l 416.29,-50.894 z" id="path18"/>
<path d="m 3807.48,1520.881 c 42.74,-5.086 70.22,-14.246 82.44,-27.476 l -2178.16,265.656 -1.02,0 c 1.02,90.586 41.73,161.836 121.12,212.723 21.37,15.269 43.77,26.465 64.12,33.59 19.34,-22.387 40.72,-38.676 66.17,-53.946 l 1.01,0 c 228,-138.422 566.94,-159.8 1014.78,-65.136 l 5.09,1.011 c 48.86,10.18 97.71,22.399 143.52,36.645 13.22,2.035 24.42,-2.035 33.58,-10.18 16.29,-12.215 36.65,-21.375 64.13,-27.476 l 0,-1.02 c 72.27,-128.25 169.97,-222.906 292.12,-284.996 84.48,-41.727 182.19,-69.215 291.1,-79.395" id="path20"/>
<path d="M 2761.15,1560.585 2860.89,31.798012 C 2716.36,589.56801 2484.29,1110.698 2165.71,1594.167 l 595.44,-33.582" id="path22"/>
<path d="m 2200.32,329.008 -4.07,-2.04 -4.07,1.02 -2.04,5.08 -82.44,1323.18799 12.21,-1.019 82.45,-1322.169 -2.04,-4.06" id="path24"/>
<path d="m 2084.29,1627.756 0,0 87.53,-1290.608 -461.08,1330.308 373.55,-39.7" id="path26"/>
</g>
<g id="g13" style="fill:none;stroke:#cccccc;stroke-width:12;stroke-linecap:round;stroke-linejoin:round">
<path d="m 2102.61,2057.9534 c -115.02,54.9606 -198.48,64.1207 -251.41,30.5387 -52.93,-35.6247 -134.36,-27.4847 -244.28,24.4219" id="path34"/>
<path d="m 2372.33,2066.093 c -21.37,-7.125 -44.78,-12.211 -69.21,-14.25 -49.87,-4.066 -109.93,15.27 -180.16,57.0042 -70.23,42.746 -154.71,59.0351 -255.48,49.8711" id="path36"/>
<path d="m 3000.34,1962.273 c -484.5,-99.742 -833.61,-76.336 -1047.35,71.25" id="path38"/>
<path d="m 3305.69,2010.117 c -21.38,-8.145 -44.79,-13.235 -69.22,-14.254 -49.87,-4.067 -109.92,15.269 -180.15,57.004 -70.23,41.7262 -154.71,58.0114 -255.48,49.8708" id="path40"/>
<path d="m 2655.29,341.21801 c -49.88,-30.54 -101.78,-22.39 -155.73,25.44 -46.82,-47.83 -95.67,-55.98 -146.57,-25.44" id="path42" style="stroke-width:29"/>
<path d="m 2542.31,170.21801 c -50.89,-30.53 -100.76,-18.32 -151.66,36.64 -49.88,-54.96 -100.76,-67.17 -151.66,-36.64" id="path44" style="stroke-width:29"/>
</g>
<g style="fill:#6c78af" id="g33">
<path d="m 56.770404,1763.8603 134.914516,0 c 40.53785,0 70.3051,11.5406 89.30315,34.6149 18.99058,23.0812 25.15303,55.2422 18.48477,96.4878 -2.73388,16.9207 -7.61168,32.4501 -14.63698,46.5933 -7.02825,14.1442 -16.30891,27.067 -27.84598,38.7573 -13.71822,14.0684 -29.07355,24.148 -46.07783,30.2307 -17.00335,6.0818 -38.74881,9.1232 -65.23042,9.1232 l -60.093245,0 -14.951878,92.5154 -70.16450702,0 56.29840402,-348.3226 z m 61.252276,55.1047 -23.613063,146.1033 42.675303,0 c 28.28742,0 49.25184,-5.7744 62.90101,-17.3383 13.63797,-11.5649 22.6337,-30.807 26.98842,-57.7355 4.19709,-25.968 1.75658,-44.2941 -7.2996,-54.9935 -9.06329,-10.6883 -26.92099,-16.036 -53.57751,-16.036 l -48.07456,0" id="path66"/>
<path d="m 378.24196,1671.3451 69.63516,0 -14.95189,92.5152 61.94982,0 c 38.98987,0 66.01185,7.3901 81.07593,22.1571 15.06891,14.7762 19.95224,38.5804 14.64264,71.4329 l -26.22037,162.2172 -70.68973,0 24.9538,-154.3893 c 2.84147,-17.5516 1.46456,-29.4876 -4.12427,-35.815 -5.58913,-6.3193 -17.28356,-9.4871 -35.06873,-9.4871 l -55.57891,0 -32.27423,199.6914 -69.64729,0 56.29807,-348.3224" id="path68"/>
<path d="m 666.43774,1763.8603 134.9135,0 c 40.54695,0 70.30511,11.5406 89.30319,34.6149 18.99454,23.0812 25.15404,55.2422 18.4898,96.4878 -2.7339,16.9207 -7.61577,32.4501 -14.63297,46.5933 -7.03329,14.1442 -16.32202,27.067 -27.85103,38.7573 -13.72229,14.0684 -29.07657,24.148 -46.08084,30.2307 -17.00032,6.0818 -38.74478,9.1232 -65.23548,9.1232 l -60.08435,0 -14.95188,92.5154 -70.16093,0 56.29099,-348.3226 z m 61.26036,55.1047 -23.61306,146.1033 42.6753,0 c 28.28641,0 49.2478,-5.7744 62.88888,-17.3383 13.641,-11.5649 22.64179,-30.807 26.99549,-57.7355 4.19406,-25.968 1.7576,-44.2941 -7.30364,-54.9935 -9.06329,-10.6883 -26.92099,-16.036 -53.56841,-16.036 l -48.07456,0" id="path70"/>
</g>
<g style="fill:#f89c0e" id="g65">
<path d="m 1027.7488,1597.3243 134.9473,0 59.2813,323.8687 163.9729,-323.8687 134.4328,0 -68.203,422.055 -90.0955,0 60.5624,-328.7447 -165.335,328.7447 -97.63,0 -62.0922,-332.1897 -44.4691,332.1897 -93.58345,0 68.21155,-422.055" id="path72"/>
<path d="m 1682.3349,1951.0787 68.129,0 39.0484,-241.6556 84.6928,0 -48.7474,301.634 c -6.6176,41.0085 -21.4426,71.3829 -44.4508,91.1237 -23.0089,19.7312 -54.9295,29.6038 -95.7507,29.6038 l -166.0776,0 10.1515,-62.7878 151.4988,0 c 16.2574,0 29.3646,-3.7991 39.3223,-11.39 9.9443,-7.6043 16.0994,-18.6108 18.427,-33.0342 l 0.8443,-5.1933 -74.9653,0 c -47.9513,0 -80.6531,-9.1406 -98.0934,-27.4219 -17.4525,-18.2801 -22.7167,-48.7649 -15.8297,-91.453 l 30.891,-191.0813 83.7372,0 -29.8633,184.7645 c -3.7745,23.3729 -2.642,38.6968 3.4343,45.9753 6.0641,7.2784 20.5937,10.9158 43.6016,10.9158" id="path74"/>
<path d="m 2102.3044,1597.3243 97.5197,0 118.1874,422.055 -102.2734,0 -23.907,-100.4601 -189.9554,0 -55.1868,100.4601 -97.5321,0 253.1476,-422.055 z m 31.6507,83.8058 -90.1808,162.444 130.2183,0 -40.0375,-162.444" id="path76"/>
<path d="m 2637.3031,2019.3793 -162.6964,0 c -49.1642,0 -85.2663,-13.9786 -108.3193,-41.9409 -23.0404,-27.9573 -30.5053,-66.9321 -22.4309,-116.9135 3.316,-20.4913 9.2287,-39.3092 17.7509,-56.4463 8.5217,-17.1419 19.771,-32.7965 33.7592,-46.9712 16.6286,-17.0415 35.3095,-29.2498 56.0305,-36.6239 20.7209,-7.3752 47.0449,-11.0604 78.9594,-11.0604 l 72.6621,0 18.1132,-112.0988 84.3865,0 -68.2152,422.055 z m -73.6376,-66.4629 28.5697,-176.7241 -51.2224,0 c -34.3524,0 -59.8397,6.9489 -76.4874,20.8321 -16.6223,13.8929 -27.5723,37.0269 -32.8017,69.397 -5.0795,31.4221 -2.0777,53.6898 9.0062,66.8122 11.0837,13.1222 32.7053,19.6828 64.8648,19.6828 l 58.0708,0" id="path78"/>
<path d="m 2761.0228,1709.4231 325.2334,0 c 47.155,0 79.7016,8.9067 97.6259,26.6992 17.9129,17.8022 23.6511,46.7028 17.1903,86.702 l -31.7718,196.555 -85.3299,0 30.0434,-185.9259 c 3.6903,-22.7947 2.5585,-37.8305 -3.3836,-45.1151 -5.966,-7.2785 -20.0182,-10.9208 -42.1807,-10.9208 l -47.5103,0 -39.1073,241.9618 -86.604,0 39.1073,-241.9618 -98.9777,0 -39.1073,241.9618 -85.3298,0 50.1021,-309.9562" id="path80"/>
<path d="m 3389.6515,1674.2008 -88.8214,0 12.4228,-76.8765 88.8214,0 -12.4228,76.8765 z m -55.7924,345.1785 -88.8215,0 50.0899,-309.9562 88.8214,0 -50.0898,309.9562" id="path82"/>
<path d="m 3457.2112,1709.4231 159.3518,0 c 48.1596,0 81.1348,8.7634 98.9115,26.2691 17.7775,17.5156 23.3935,46.5596 16.8359,87.1321 l -31.7597,196.555 -84.705,0 30.1524,-186.5005 c 3.7421,-23.1817 2.4511,-38.2189 -3.8732,-45.1102 -6.325,-6.9 -20.6913,-10.3511 -43.0866,-10.3511 l -68.1168,0 -39.1072,241.9618 -84.6928,0 50.0897,-309.9562" id="path84"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

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 @@
{ {
"/app.js": "/app.js?id=2ca3fa12f273bd645611f1acf3d81355", "/app.js": "/app.js?id=5f574f36f456b103dffcfa21d5612785",
"/app.css": "/app.css?id=93151d8b186ef7758df8582425ff8082", "/app.css": "/app.css?id=b701a4344131bb2c00e9f0b1ef1ab3c1",
"/img/log-viewer-128.png": "/img/log-viewer-128.png?id=d576c6d2e16074d3f064e60fe4f35166", "/img/log-viewer-128.png": "/img/log-viewer-128.png?id=d576c6d2e16074d3f064e60fe4f35166",
"/img/log-viewer-32.png": "/img/log-viewer-32.png?id=f8ec67d10f996aa8baf00df3b61eea6d", "/img/log-viewer-32.png": "/img/log-viewer-32.png?id=f8ec67d10f996aa8baf00df3b61eea6d",
"/img/log-viewer-64.png": "/img/log-viewer-64.png?id=8902d596fc883ca9eb8105bb683568c6" "/img/log-viewer-64.png": "/img/log-viewer-64.png?id=8902d596fc883ca9eb8105bb683568c6"

View File

@ -1,5 +1,5 @@
server { server {
listen 54331; listen __port__;
server_name _; server_name _;
root /home/vito/phpmyadmin; root /home/vito/phpmyadmin;

File diff suppressed because one or more lines are too long

View File

@ -37,15 +37,25 @@
<div <div
class="left-0 top-0 min-h-screen w-64 flex-none bg-gray-800 dark:bg-gray-800/50 p-3 dark:border-r-2 dark:border-gray-800"> class="left-0 top-0 min-h-screen w-64 flex-none bg-gray-800 dark:bg-gray-800/50 p-3 dark:border-r-2 dark:border-gray-800">
<div class="h-16 block"> <div class="h-16 block">
<div class="flex items-center justify-start text-3xl font-extrabold text-white"> <div class="flex items-center justify-start text-2xl font-extrabold text-white">
Vito <x-application-logo class="w-7 h-7 rounded-md" />
<span class="ml-1">Deploy</span>
</div> </div>
</div> </div>
<div class="mb-5 space-y-2"> <div class="mb-5">
<div class="uppercase text-gray-300 text-sm font-semibold">{{ __("Projects") }}</div>
<div class="mt-2">
@include('layouts.partials.project-select', ['project' => auth()->user()->currentProject])
</div>
<div class="mt-5 uppercase text-gray-300 text-sm font-semibold">{{ __("Servers") }}</div>
<div class="mt-2">
@include('layouts.partials.server-select', ['server' => isset($server) ? $server : null]) @include('layouts.partials.server-select', ['server' => isset($server) ? $server : null])
</div>
@if (isset($server)) @if (isset($server))
<div class="mt-3 space-y-1">
<x-sidebar-link :href="route('servers.show', ['server' => $server])" :active="request()->routeIs('servers.show')"> <x-sidebar-link :href="route('servers.show', ['server' => $server])" :active="request()->routeIs('servers.show')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6"> stroke="currentColor" class="w-6 h-6">
@ -141,6 +151,7 @@ class="left-0 top-0 min-h-screen w-64 flex-none bg-gray-800 dark:bg-gray-800/50
</svg> </svg>
<span class="ml-2 text-gray-50">{{ __('Logs') }}</span> <span class="ml-2 text-gray-50">{{ __('Logs') }}</span>
</x-sidebar-link> </x-sidebar-link>
</div>
@endif @endif
</div> </div>
</div> </div>
@ -160,45 +171,7 @@ class="min-h-screen w-64 flex-none border-r border-gray-200 bg-white dark:border
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
{{-- Search --}} {{-- Search --}}
</div> </div>
{{-- Dark Mode Toggle Button section --}} @include('layouts.partials.color-scheme')
<div class="flex items-center" x-data="{
isDarkMode: localStorage.theme,
toggleTheme() {
localStorage.theme = this.isDarkMode == 'dark' ? 'light' : 'dark';
if (localStorage.theme === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
this.isDarkMode = localStorage.theme
}
}" x-on:click="toggleTheme()">
<div class="flex items-center">
<div class="flex items-center justify-end">
<button id="theme-toggle" type="button" class="text-sm p-2"
:class="isDarkMode == 'dark' ? 'text-gray-300 border-gray-300' :
'text-gray-800 border-gray-800'">
<svg x-show="isDarkMode!='dark'" id="theme-toggle-dark-icon" class="w-5 h-5"
fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path
d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z">
</path>
</svg>
<svg x-show="isDarkMode=='dark'" id="theme-toggle-light-icon" class="w-5 h-5"
fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
fillRule="evenodd" clipRule="evenodd"></path>
</svg>
</button>
</div>
</div>
</div>
{{-- End of Dark Mode Toggle Button section --}}
<div class="ml-6 flex items-center"> <div class="ml-6 flex items-center">
<div class="relative ml-5"> <div class="relative ml-5">
<x-dropdown align="right" width="48"> <x-dropdown align="right" width="48">

View File

@ -20,11 +20,14 @@
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900"> <div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
<div> <div>
<a href="/"> <a href="/">
<x-application-logo class="w-20 h-20 fill-current text-gray-500 rounded-lg" /> <div class="flex items-center justify-start text-3xl font-extrabold">
<x-application-logo class="w-9 h-9 rounded-md" />
<span class="ml-1">Deploy</span>
</div>
</a> </a>
</div> </div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden rounded-lg"> <div class="w-full sm:max-w-md mt-10 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden rounded-lg">
{{ $slot }} {{ $slot }}
</div> </div>
</div> </div>

View File

@ -0,0 +1,37 @@
<div class="flex items-center" x-data="{
isDarkMode: localStorage.theme,
toggleTheme() {
localStorage.theme = this.isDarkMode === 'dark' ? 'light' : 'dark';
if (localStorage.theme === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
this.isDarkMode = localStorage.theme
}
}" x-on:click="toggleTheme()">
<div class="flex items-center">
<div class="flex items-center justify-end">
<button id="theme-toggle" type="button" class="text-sm p-2"
:class="isDarkMode === 'dark' ? 'text-gray-300 border-gray-300' :
'text-gray-800 border-gray-800'">
<svg x-show="isDarkMode !== 'dark'" id="theme-toggle-dark-icon" class="w-5 h-5"
fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path
d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z">
</path>
</svg>
<svg x-show="isDarkMode === 'dark'" id="theme-toggle-light-icon" class="w-5 h-5"
fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
fillRule="evenodd" clipRule="evenodd"></path>
</svg>
</button>
</div>
</div>
</div>

View File

@ -0,0 +1,90 @@
<div x-data="projectCombobox()">
<div class="relative">
<div @click="open = !open" class="z-0 w-full cursor-pointer px-4 py-3 pr-10 text-md leading-5 text-gray-100 focus:ring-1 focus:ring-gray-700 bg-gray-900 rounded-md h-10 flex items-center" x-text="selected.name ?? 'Select Project'"></div>
<button type="button" @click="open = !open" class="z-0 absolute inset-y-0 right-0 flex items-center pr-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="h-5 w-5 text-gray-400"><path fill-rule="evenodd" d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" clip-rule="evenodd"></path></svg>
</button>
<div
x-show="open"
@click.away="open = false"
class="z-10 absolute mt-1 w-full overflow-auto rounded-md pb-1 bg-white dark:bg-gray-700 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
<div class="p-2 relative">
<input x-model="query"
@input="filterProjectsAndOpen"
placeholder="Filter"
class="w-full py-2 pl-3 pr-10 text-sm leading-5 dark:text-gray-100 focus:ring-1 focus:ring-gray-400 dark:focus:ring-800 bg-gray-200 dark:bg-gray-900 rounded-md"
>
</div>
<div class="relative max-h-[350px] overflow-y-auto">
<template x-for="(project, index) in filteredProjects" :key="index">
<div
@click="selectProject(project); open = false"
:class="project.id === selected.id ? 'cursor-default bg-primary-600 text-white' : 'cursor-pointer'"
class="relative select-none py-2 px-4 text-gray-700 dark:text-white hover:bg-primary-600 hover:text-white">
<span class="block truncate" x-text="project.name"></span>
<template x-if="project.id === selected.id">
<span class="absolute inset-y-0 right-0 flex items-center pr-3 text-white">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="h-5 w-5"><path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd"></path></svg>
</span>
</template>
</div>
</template>
</div>
<div
x-show="filteredProjects.length === 0"
class="relative cursor-default select-none py-2 px-4 text-gray-700 dark:text-white block truncate">
No projects found!
</div>
<div class="py-1">
<hr class="border-gray-300 dark:border-gray-600">
</div>
<div>
<a
href="{{ route('projects') }}"
class="relative select-none py-2 px-4 text-gray-700 dark:text-white hover:bg-primary-600 hover:text-white block cursor-pointer">
<span class="block truncate">Projects List</span>
</a>
</div>
<div>
<a
href="{{ route('projects', ['create' => true]) }}"
class="relative select-none py-2 px-4 text-gray-700 dark:text-white hover:bg-primary-600 hover:text-white block cursor-pointer">
<span class="block truncate">Create a Project</span>
</a>
</div>
</div>
</div>
</div>
<script>
function projectCombobox() {
const projects = @json(auth()->user()->projects()->select('id', 'name')->get());
return {
open: false,
query: '',
projects: projects,
selected: @if(isset($project)) @json($project->only('id', 'name')) @else {} @endif,
filteredProjects: projects,
selectProject(project) {
if (this.selected.id !== project.id) {
this.selected = project;
window.location.href = '{{ url('/settings/projects/') }}/' + project.id
}
},
filterProjectsAndOpen() {
if (this.query === '') {
this.filteredProjects = this.projects;
this.open = false;
} else {
this.filteredProjects = this.projects.filter((project) =>
project.name
.toLowerCase()
.replace(/\s+/g, '')
.includes(this.query.toLowerCase().replace(/\s+/g, ''))
);
this.open = true;
}
},
};
}
</script>

View File

@ -1,13 +1,13 @@
<div x-data="serverCombobox()"> <div x-data="serverCombobox()">
<div class="relative"> <div class="relative">
<div @click="open = !open" class="w-full cursor-pointer px-4 py-3 pr-10 text-md leading-5 text-gray-100 focus:ring-1 focus:ring-gray-700 bg-gray-900 rounded-md h-10 flex items-center" x-text="selected.name ?? 'Select Server'"></div> <div @click="open = !open" class="z-0 w-full cursor-pointer px-4 py-3 pr-10 text-md leading-5 text-gray-100 focus:ring-1 focus:ring-gray-700 bg-gray-900 rounded-md h-10 flex items-center" x-text="selected.name ?? 'Select Server'"></div>
<button type="button" @click="open = !open" class="absolute inset-y-0 right-0 flex items-center pr-2"> <button type="button" @click="open = !open" class="z-0 absolute inset-y-0 right-0 flex items-center pr-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="h-5 w-5 text-gray-400"><path fill-rule="evenodd" d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" clip-rule="evenodd"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="h-5 w-5 text-gray-400"><path fill-rule="evenodd" d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" clip-rule="evenodd"></path></svg>
</button> </button>
<div <div
x-show="open" x-show="open"
@click.away="open = false" @click.away="open = false"
class="absolute mt-1 w-full overflow-auto rounded-md pb-1 bg-white dark:bg-gray-700 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"> class="z-10 absolute mt-1 w-full overflow-auto rounded-md pb-1 bg-white dark:bg-gray-700 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
<div class="p-2 relative"> <div class="p-2 relative">
<input x-model="query" <input x-model="query"
@input="filterServersAndOpen" @input="filterServersAndOpen"
@ -58,7 +58,7 @@ class="relative select-none py-2 px-4 text-gray-700 dark:text-white hover:bg-pri
<script> <script>
function serverCombobox() { function serverCombobox() {
const servers = @json(\App\Models\Server::query()->select('id', 'name')->get()); const servers = @json(auth()->user()->currentProject->servers()->select('id', 'name')->get());
return { return {
open: false, open: false,
query: '', query: '',

View File

@ -16,6 +16,12 @@
</svg> </svg>
{{ __('Profile') }} {{ __('Profile') }}
</x-secondary-sidebar-link> </x-secondary-sidebar-link>
<x-secondary-sidebar-link :href="route('projects')" :active="request()->routeIs('projects')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-2">
<path stroke-linecap="round" stroke-linejoin="round" d="m7.875 14.25 1.214 1.942a2.25 2.25 0 0 0 1.908 1.058h2.006c.776 0 1.497-.4 1.908-1.058l1.214-1.942M2.41 9h4.636a2.25 2.25 0 0 1 1.872 1.002l.164.246a2.25 2.25 0 0 0 1.872 1.002h2.092a2.25 2.25 0 0 0 1.872-1.002l.164-.246A2.25 2.25 0 0 1 16.954 9h4.636M2.41 9a2.25 2.25 0 0 0-.16.832V12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 12V9.832c0-.287-.055-.57-.16-.832M2.41 9a2.25 2.25 0 0 1 .382-.632l3.285-3.832a2.25 2.25 0 0 1 1.708-.786h8.43c.657 0 1.281.287 1.709.786l3.284 3.832c.163.19.291.404.382.632M4.5 20.25h15A2.25 2.25 0 0 0 21.75 18v-2.625c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125V18a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>
{{ __('Projects') }}
</x-secondary-sidebar-link>
<x-secondary-sidebar-link :href="route('server-providers')" :active="request()->routeIs('server-providers')"> <x-secondary-sidebar-link :href="route('server-providers')" :active="request()->routeIs('server-providers')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-2"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z" />

View File

@ -1,3 +1,4 @@
@php use App\Enums\SiteFeature; @endphp
<x-app-layout :server="$site->server"> <x-app-layout :server="$site->server">
@if(isset($pageTitle)) @if(isset($pageTitle))
<x-slot name="pageTitle">{{ $site->domain }} - {{ $pageTitle }}</x-slot> <x-slot name="pageTitle">{{ $site->domain }} - {{ $pageTitle }}</x-slot>
@ -58,10 +59,12 @@
<x-secondary-sidebar-link :href="route('servers.sites.show', ['server' => $site->server, 'site' => $site])" :active="request()->routeIs('servers.sites.show')"> <x-secondary-sidebar-link :href="route('servers.sites.show', ['server' => $site->server, 'site' => $site])" :active="request()->routeIs('servers.sites.show')">
{{ __('Application') }} {{ __('Application') }}
</x-secondary-sidebar-link> </x-secondary-sidebar-link>
@if($site->status == \App\Enums\SiteStatus::READY) @if($site->isReady() && $site->hasFeature(SiteFeature::SSL))
<x-secondary-sidebar-link :href="route('servers.sites.ssl', ['server' => $site->server, 'site' => $site])" :active="request()->routeIs('servers.sites.ssl')"> <x-secondary-sidebar-link :href="route('servers.sites.ssl', ['server' => $site->server, 'site' => $site])" :active="request()->routeIs('servers.sites.ssl')">
{{ __('SSL') }} {{ __('SSL') }}
</x-secondary-sidebar-link> </x-secondary-sidebar-link>
@endif
@if($site->isReady() && $site->hasFeature(SiteFeature::QUEUES))
<x-secondary-sidebar-link :href="route('servers.sites.queues', ['server' => $site->server, 'site' => $site])" :active="request()->routeIs('servers.sites.queues')"> <x-secondary-sidebar-link :href="route('servers.sites.queues', ['server' => $site->server, 'site' => $site])" :active="request()->routeIs('servers.sites.queues')">
{{ __('Queues') }} {{ __('Queues') }}
</x-secondary-sidebar-link> </x-secondary-sidebar-link>

View File

@ -0,0 +1,32 @@
<div>
@if($site->deploymentScript?->content)
<x-dropdown>
<x-slot name="trigger">
<x-secondary-button>
{{ __('Auto Deployment') }}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 ml-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
</svg>
</x-secondary-button>
</x-slot>
<x-slot name="content">
<x-dropdown-link class="cursor-pointer" wire:click="enable">
{{ __("Enable") }}
@if($site->auto_deployment)
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 ml-1 text-green-600">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
@endif
</x-dropdown-link>
<x-dropdown-link class="cursor-pointer" wire:click="disable">
{{ __("Disable") }}
@if(!$site->auto_deployment)
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 ml-1 text-green-600">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
@endif
</x-dropdown-link>
</x-slot>
</x-dropdown>
@endif
</div>

View File

@ -1,5 +1,4 @@
<div x-data=""> <div x-data="">
<x-secondary-button x-on:click="$dispatch('open-modal', 'change-branch')">{{ __("Branch") }}</x-secondary-button>
<x-modal name="change-branch"> <x-modal name="change-branch">
<form wire:submit.prevent="change" class="p-6"> <form wire:submit.prevent="change" class="p-6">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100"> <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">

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