deploy Wordpress sites via VitoDeploy (#83)

This commit is contained in:
Saeed Vaziry 2024-01-01 16:20:57 +01:00 committed by GitHub
parent 5e6d338bdc
commit 4cda14f4b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 443 additions and 292 deletions

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

@ -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;

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

@ -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

@ -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

@ -6,6 +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\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;
@ -18,6 +19,7 @@
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\Support\Facades\Log;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Throwable; use Throwable;
@ -392,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

@ -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',
@ -44,9 +52,9 @@ public function createFields(array $input): array
{ {
return [ return [
'web_directory' => $input['web_directory'] ?? '', 'web_directory' => $input['web_directory'] ?? '',
'source_control_id' => $input['source_control'] ?? '', 'source_control_id' => $input['source_control'],
'repository' => $input['repository'] ?? '', 'repository' => $input['repository'],
'branch' => $input['branch'] ?? '', 'branch' => $input['branch'],
]; ];
} }
@ -54,6 +62,7 @@ public function data(array $input): array
{ {
return [ return [
'composer' => (bool) $input['composer'], 'composer' => (bool) $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

@ -25,6 +25,7 @@
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\Github; use App\SourceControlProviders\Github;
use App\SourceControlProviders\Gitlab; use App\SourceControlProviders\Gitlab;
@ -262,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,
], ],
/* /*

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-328222da.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

@ -161,45 +161,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

@ -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

@ -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

@ -1,3 +1,6 @@
<div> <div>
<x-simple-card class="flex items-center justify-between">
<span>{{ __("Your Wordpress site is installed and ready to use! ") }}</span>
<x-secondary-button :href="$site->url" target="_blank">{{ __("Open Website") }}</x-secondary-button>
</x-simple-card>
</div> </div>

View File

@ -5,10 +5,10 @@
<form id="create-site" wire:submit.prevent="create" class="space-y-6"> <form id="create-site" wire:submit.prevent="create" class="space-y-6">
<div> <div>
<x-input-label>{{ __("Select site type") }}</x-input-label> <x-input-label>{{ __("Select site type") }}</x-input-label>
<x-select-input wire:model="type" id="type" name="type" class="mt-1 w-full"> <x-select-input wire:model="inputs.type" id="type" name="type" class="mt-1 w-full">
<option value="" selected disabled>{{ __("Select") }}</option> <option value="" selected disabled>{{ __("Select") }}</option>
@foreach(config('core.site_types') as $t) @foreach(config('core.site_types') as $t)
<option value="{{ $t }}" @if($t === $type) selected @endif> <option value="{{ $t }}" @if($t === $inputs['type']) selected @endif>
{{ $t }} {{ $t }}
</option> </option>
@endforeach @endforeach
@ -20,7 +20,7 @@
<div> <div>
<x-input-label for="domain" :value="__('Domain')" /> <x-input-label for="domain" :value="__('Domain')" />
<x-text-input wire:model.defer="domain" id="domain" name="domain" type="text" class="mt-1 block w-full" autocomplete="domain" placeholder="example.com" /> <x-text-input wire:model.defer="inputs.domain" id="domain" name="domain" type="text" class="mt-1 block w-full" autocomplete="domain" placeholder="example.com" />
@error('domain') @error('domain')
<x-input-error class="mt-2" :messages="$message" /> <x-input-error class="mt-2" :messages="$message" />
@enderror @enderror
@ -28,75 +28,15 @@
<div> <div>
<x-input-label for="alias" :value="__('Alias')" /> <x-input-label for="alias" :value="__('Alias')" />
<x-text-input wire:model.defer="alias" id="alias" name="alias" type="text" class="mt-1 block w-full" autocomplete="alias" placeholder="www.example.com" /> <x-text-input wire:model.defer="inputs.alias" id="alias" name="alias" type="text" class="mt-1 block w-full" autocomplete="alias" placeholder="www.example.com" />
@error('alias') @error('alias')
<x-input-error class="mt-2" :messages="$message" /> <x-input-error class="mt-2" :messages="$message" />
@enderror @enderror
</div> </div>
<div> @if (isset($inputs['type']) && $inputs['type'])
<x-input-label for="php_version" :value="__('PHP Version')" /> @include('livewire.sites.partials.create.' . $inputs['type'])
<x-select-input wire:model.defer="php_version" id="php_version" name="php_version" class="mt-1 w-full"> @endif
<option value="" selected disabled>{{ __("Select") }}</option>
@foreach($server->installedPHPVersions() as $version)
<option value="{{ $version }}" @if($version === $php_version) selected @endif>
PHP {{ $version }}
</option>
@endforeach
</x-select-input>
@error('php_version')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="web_directory" :value="__('Web Directory')" />
<x-text-input wire:model.defer="web_directory" id="web_directory" name="web_directory" type="text" class="mt-1 block w-full" autocomplete="web_directory" />
@error('web_directory')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="source_control" :value="__('Source Control')" />
<div class="flex items-center mt-1">
<x-select-input wire:model="source_control" id="source_control" name="source_control" class="mt-1 w-full">
<option value="" selected disabled>{{ __("Select") }}</option>
@foreach($sourceControls as $sourceControl)
<option value="{{ $sourceControl->id }}" @if($sourceControl->id === $source_control) selected @endif>
{{ $sourceControl->profile }} ({{ $sourceControl->provider }})
</option>
@endforeach
</x-select-input>
<x-secondary-button :href="route('source-controls', ['redirect' => request()->url()])" class="flex-none ml-2">{{ __('Connect') }}</x-secondary-button>
</div>
@error('source_control')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="repository" :value="__('Repository')" />
<x-text-input wire:model.defer="repository" id="repository" name="repository" type="text" class="mt-1 block w-full" autocomplete="repository" placeholder="organization/repository" />
@error('repository')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="branch" :value="__('Branch')" />
<x-text-input wire:model.defer="branch" id="branch" name="branch" type="text" class="mt-1 block w-full" autocomplete="branch" placeholder="main" />
@error('branch')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<label for="composer" class="inline-flex items-center">
<input id="composer" wire:model.defer="composer" type="checkbox" class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" name="composer">
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Run `composer install --no-dev`') }}</span>
</label>
</div>
</form> </form>
<x-slot name="actions"> <x-slot name="actions">
<x-primary-button form="create-site" wire:loading.attr="disabled">{{ __('Create') }}</x-primary-button> <x-primary-button form="create-site" wire:loading.attr="disabled">{{ __('Create') }}</x-primary-button>

View File

@ -0,0 +1,7 @@
<div>
<x-input-label for="branch" :value="__('Branch')" />
<x-text-input wire:model.defer="inputs.branch" id="branch" name="branch" type="text" class="mt-1 block w-full" autocomplete="branch" placeholder="main" />
@error('branch')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>

View File

@ -0,0 +1,6 @@
<div class="mt-6">
<label for="composer" class="inline-flex items-center">
<input id="composer" wire:model.defer="inputs.composer" type="checkbox" class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" name="composer">
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Run `composer install --no-dev`') }}</span>
</label>
</div>

View File

@ -0,0 +1,17 @@
@php
/* @var \App\Models\Server $server */
@endphp
<div>
<x-input-label for="php_version" :value="__('PHP Version')" />
<x-select-input wire:model.defer="inputs.php_version" id="php_version" name="php_version" class="mt-1 w-full">
<option value="" selected>{{ __("Select") }}</option>
@foreach($server->installedPHPVersions() as $version)
<option value="{{ $version }}" @if($version === $inputs['php_version']) selected @endif>
PHP {{ $version }}
</option>
@endforeach
</x-select-input>
@error('php_version')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>

View File

@ -0,0 +1,7 @@
<div>
<x-input-label for="repository" :value="__('Repository')" />
<x-text-input wire:model.defer="inputs.repository" id="repository" name="repository" type="text" class="mt-1 block w-full" autocomplete="repository" placeholder="organization/repository" />
@error('repository')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>

View File

@ -0,0 +1,17 @@
<div>
<x-input-label for="source_control" :value="__('Source Control')" />
<div class="flex items-center mt-1">
<x-select-input wire:model="inputs.source_control" id="source_control" name="source_control" class="mt-1 w-full">
<option value="" selected>{{ __("Select") }}</option>
@foreach($sourceControls as $sourceControl)
<option value="{{ $sourceControl->id }}" @if($sourceControl->id === $inputs['source_control']) selected @endif>
{{ $sourceControl->profile }} ({{ $sourceControl->provider }})
</option>
@endforeach
</x-select-input>
<x-secondary-button :href="route('source-controls', ['redirect' => request()->url()])" class="flex-none ml-2">{{ __('Connect') }}</x-secondary-button>
</div>
@error('source_control')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>

View File

@ -0,0 +1,10 @@
<div>
<x-input-label for="web_directory" :value="__('Web Directory')" />
<x-text-input wire:model.defer="inputs.web_directory" id="web_directory" name="web_directory" type="text" class="mt-1 block w-full" autocomplete="web_directory" />
<x-input-help>
{{ __("For root, leave this blank") }}
</x-input-help>
@error('web_directory')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>

View File

@ -0,0 +1,11 @@
@include('livewire.sites.partials.create.fields.php-version')
@include('livewire.sites.partials.create.fields.web-directory')
@include('livewire.sites.partials.create.fields.source-control')
@include('livewire.sites.partials.create.fields.repository')
@include('livewire.sites.partials.create.fields.branch')
@include('livewire.sites.partials.create.fields.composer')

View File

@ -0,0 +1,11 @@
@include('livewire.sites.partials.create.fields.php-version')
@include('livewire.sites.partials.create.fields.web-directory')
@include('livewire.sites.partials.create.fields.source-control')
@include('livewire.sites.partials.create.fields.repository')
@include('livewire.sites.partials.create.fields.branch')
@include('livewire.sites.partials.create.fields.composer')

View File

@ -0,0 +1,63 @@
@include('livewire.sites.partials.create.fields.php-version')
<div>
<x-input-label for="title" :value="__('Title')" />
<x-text-input wire:model.defer="inputs.title" id="title" name="title" type="text" class="mt-1 block w-full" autocomplete="branch" />
@error('title')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
<div>
<x-input-label for="email" :value="__('WP Admin Email')" />
<x-text-input wire:model.defer="inputs.email" id="email" name="email" type="email" class="mt-1 block w-full" autocomplete="email" />
@error('email')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="username" :value="__('WP Admin Username')" />
<x-text-input wire:model.defer="inputs.username" id="username" name="username" type="text" class="mt-1 block w-full" autocomplete="username" />
@error('username')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="password" :value="__('WP Admin Password')" />
<x-text-input wire:model.defer="inputs.password" id="password" name="password" type="text" class="mt-1 block w-full" />
@error('title')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
<div>
<x-input-label for="database" :value="__('Database Name')" />
<x-text-input wire:model.defer="inputs.database" id="database" name="database" type="text" class="mt-1 block w-full" autocomplete="database" />
<x-input-help>{{ __("It will create a database with this name") }}</x-input-help>
@error('database')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="database" :value="__('Database User')" />
<x-text-input wire:model.defer="inputs.database_user" id="database_user" name="database_user" type="text" class="mt-1 block w-full" autocomplete="database_user" />
<x-input-help>{{ __("It will create a database user with this username") }}</x-input-help>
@error('database_user')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="password" :value="__('Database Password')" />
<x-text-input wire:model.defer="inputs.database_password" id="database_password" name="database_password" type="text" class="mt-1 block w-full" />
@error('database_password')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>

View File

@ -22,7 +22,10 @@ class SitesTest extends TestCase
{ {
use RefreshDatabase; use RefreshDatabase;
public function test_create_site(): void /**
* @dataProvider create_data
*/
public function test_create_site(array $inputs): void
{ {
Bus::fake(); Bus::fake();
@ -36,15 +39,8 @@ public function test_create_site(): void
]); ]);
Livewire::test(CreateSite::class, ['server' => $this->server]) Livewire::test(CreateSite::class, ['server' => $this->server])
->set('type', SiteType::LARAVEL) ->fill($inputs)
->set('domain', 'example.com') ->set('inputs.source_control', $sourceControl->id)
->set('alias', 'www.example.com')
->set('php_version', '8.2')
->set('web_directory', 'public')
->set('source_control', $sourceControl->id)
->set('repository', 'test/test')
->set('branch', 'main')
->set('composer', true)
->call('create') ->call('create')
->assertSuccessful() ->assertSuccessful()
->assertHasNoErrors(); ->assertHasNoErrors();
@ -127,4 +123,37 @@ public function test_update_source_control(): void
$this->assertEquals($gitlab->id, $this->site->source_control_id); $this->assertEquals($gitlab->id, $this->site->source_control_id);
} }
public static function create_data(): array
{
return [
[
[
'inputs.type' => SiteType::LARAVEL,
'inputs.domain' => 'example.com',
'inputs.alias' => 'www.example.com',
'inputs.php_version' => '8.2',
'inputs.web_directory' => 'public',
'inputs.repository' => 'test/test',
'inputs.branch' => 'main',
'inputs.composer' => true,
],
],
[
[
'inputs.type' => SiteType::WORDPRESS,
'inputs.domain' => 'example.com',
'inputs.alias' => 'www.example.com',
'inputs.php_version' => '8.2',
'inputs.title' => 'Example',
'inputs.username' => 'example',
'inputs.email' => 'email@example.com',
'inputs.password' => 'password',
'inputs.database' => 'example',
'inputs.database_user' => 'example',
'inputs.database_password' => 'password',
],
],
];
}
} }