mirror of
https://github.com/vitodeploy/vito.git
synced 2025-04-20 10:21:37 +00:00
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
fb651ab5ce | ||
|
c2625a7352 | ||
|
b72a2ddb1c | ||
|
0e8e6ef56f | ||
|
39fa25aee7 | ||
|
945c2e75c0 | ||
|
82933e29ff | ||
|
82c1f36ef6 | ||
|
e06d23b31a | ||
|
f0e7faa0e7 | ||
|
319fdb44e7 | ||
|
b62c40c97d | ||
|
e39e8c17a2 | ||
|
1391eb32d8 | ||
|
7f5e68e131 | ||
|
431da1b728 | ||
|
8c487a64fa | ||
|
a67e586a5d | ||
|
960db714b7 | ||
|
7da0221ccb |
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Feature request
|
||||
url: https://github.com/vitodeploy/vito/discussions/new?category=ideas
|
||||
about: Share ideas for new features
|
||||
- name: Support
|
||||
url: https://github.com/vitodeploy/vito/discussions/new?category=q-a
|
||||
about: Ask the community for help
|
||||
- name: Discord
|
||||
url: https://discord.gg/uZeeHZZnm5
|
||||
about: Join the community
|
12
.github/ISSUE_TEMPLATE/feature_request.md
vendored
12
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,12 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
To request a feature or suggest an idea please add it to the feedback boards
|
||||
|
||||
https://vitodeploy.featurebase.app/
|
@ -34,8 +34,7 @@ ## Useful Links
|
||||
- [Documentation](https://vitodeploy.com)
|
||||
- [Install on Server](https://vitodeploy.com/introduction/installation.html#install-on-vps-recommended)
|
||||
- [Install via Docker](https://vitodeploy.com/introduction/installation.html#install-via-docker)
|
||||
- [Feedbacks](https://vitodeploy.featurebase.app)
|
||||
- [Roadmap](https://vitodeploy.featurebase.app/roadmap)
|
||||
- [Roadmap](https://github.com/orgs/vitodeploy/projects/5)
|
||||
- [Video Demo](https://youtu.be/AbmUOBDOc28)
|
||||
- [Discord](https://discord.gg/uZeeHZZnm5)
|
||||
- [Contribution](/CONTRIBUTING.md)
|
||||
|
58
app/Actions/Tag/AttachTag.php
Normal file
58
app/Actions/Tag/AttachTag.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Tag;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\Site;
|
||||
use App\Models\Tag;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AttachTag
|
||||
{
|
||||
public function attach(User $user, array $input): Tag
|
||||
{
|
||||
$this->validate($input);
|
||||
|
||||
/** @var Server|Site $taggable */
|
||||
$taggable = $input['taggable_type']::findOrFail($input['taggable_id']);
|
||||
|
||||
$tag = Tag::query()->where('name', $input['name'])->first();
|
||||
if ($tag) {
|
||||
if (! $taggable->tags->contains($tag->id)) {
|
||||
$taggable->tags()->attach($tag->id);
|
||||
}
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
$tag = new Tag([
|
||||
'project_id' => $user->currentProject->id,
|
||||
'name' => $input['name'],
|
||||
'color' => config('core.tag_colors')[array_rand(config('core.tag_colors'))],
|
||||
]);
|
||||
$tag->save();
|
||||
|
||||
$taggable->tags()->attach($tag->id);
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
private function validate(array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => [
|
||||
'required',
|
||||
],
|
||||
'taggable_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
],
|
||||
'taggable_type' => [
|
||||
'required',
|
||||
Rule::in(config('core.taggable_types')),
|
||||
],
|
||||
])->validate();
|
||||
}
|
||||
}
|
49
app/Actions/Tag/CreateTag.php
Normal file
49
app/Actions/Tag/CreateTag.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Tag;
|
||||
|
||||
use App\Models\Tag;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class CreateTag
|
||||
{
|
||||
public function create(User $user, array $input): Tag
|
||||
{
|
||||
$this->validate($input);
|
||||
|
||||
$tag = Tag::query()
|
||||
->where('project_id', $user->current_project_id)
|
||||
->where('name', $input['name'])
|
||||
->first();
|
||||
if ($tag) {
|
||||
throw ValidationException::withMessages([
|
||||
'name' => ['Tag with this name already exists.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$tag = new Tag([
|
||||
'project_id' => $user->currentProject->id,
|
||||
'name' => $input['name'],
|
||||
'color' => $input['color'],
|
||||
]);
|
||||
$tag->save();
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
private function validate(array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => [
|
||||
'required',
|
||||
],
|
||||
'color' => [
|
||||
'required',
|
||||
Rule::in(config('core.tag_colors')),
|
||||
],
|
||||
])->validate();
|
||||
}
|
||||
}
|
15
app/Actions/Tag/DeleteTag.php
Normal file
15
app/Actions/Tag/DeleteTag.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Tag;
|
||||
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DeleteTag
|
||||
{
|
||||
public function delete(Tag $tag): void
|
||||
{
|
||||
DB::table('taggables')->where('tag_id', $tag->id)->delete();
|
||||
$tag->delete();
|
||||
}
|
||||
}
|
36
app/Actions/Tag/DetachTag.php
Normal file
36
app/Actions/Tag/DetachTag.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Tag;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\Site;
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class DetachTag
|
||||
{
|
||||
public function detach(Tag $tag, array $input): void
|
||||
{
|
||||
$this->validate($input);
|
||||
|
||||
/** @var Server|Site $taggable */
|
||||
$taggable = $input['taggable_type']::findOrFail($input['taggable_id']);
|
||||
|
||||
$taggable->tags()->detach($tag->id);
|
||||
}
|
||||
|
||||
private function validate(array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'taggable_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
],
|
||||
'taggable_type' => [
|
||||
'required',
|
||||
Rule::in(config('core.taggable_types')),
|
||||
],
|
||||
])->validate();
|
||||
}
|
||||
}
|
38
app/Actions/Tag/EditTag.php
Normal file
38
app/Actions/Tag/EditTag.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Tag;
|
||||
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class EditTag
|
||||
{
|
||||
public function edit(Tag $tag, array $input): void
|
||||
{
|
||||
$this->validate($input);
|
||||
|
||||
$tag->name = $input['name'];
|
||||
$tag->color = $input['color'];
|
||||
|
||||
$tag->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validate(array $input): void
|
||||
{
|
||||
$rules = [
|
||||
'name' => [
|
||||
'required',
|
||||
],
|
||||
'color' => [
|
||||
'required',
|
||||
Rule::in(config('core.tag_colors')),
|
||||
],
|
||||
];
|
||||
Validator::make($input, $rules)->validate();
|
||||
}
|
||||
}
|
@ -9,4 +9,8 @@ final class StorageProvider
|
||||
const FTP = 'ftp';
|
||||
|
||||
const LOCAL = 'local';
|
||||
|
||||
const S3 = 's3';
|
||||
|
||||
const WASABI = 'wasabi';
|
||||
}
|
||||
|
30
app/Facades/FTP.php
Normal file
30
app/Facades/FTP.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use App\Support\Testing\FTPFake;
|
||||
use FTP\Connection;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @method static bool|Connection connect(string $host, string $port, bool $ssl = false)
|
||||
* @method static bool login(string $username, string $password, bool|Connection $connection)
|
||||
* @method static void close(bool|Connection $connection)
|
||||
* @method static bool passive(bool|Connection $connection, bool $passive)
|
||||
* @method static bool delete(bool|Connection $connection, string $path)
|
||||
* @method static void assertConnected(string $host)
|
||||
*/
|
||||
class FTP extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return 'ftp';
|
||||
}
|
||||
|
||||
public static function fake(): FTPFake
|
||||
{
|
||||
static::swap($fake = new FTPFake());
|
||||
|
||||
return $fake;
|
||||
}
|
||||
}
|
37
app/Helpers/FTP.php
Normal file
37
app/Helpers/FTP.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use FTP\Connection;
|
||||
|
||||
class FTP
|
||||
{
|
||||
public function connect(string $host, string $port, bool $ssl = false): bool|Connection
|
||||
{
|
||||
if ($ssl) {
|
||||
return ftp_ssl_connect($host, $port, 5);
|
||||
}
|
||||
|
||||
return ftp_connect($host, $port, 5);
|
||||
}
|
||||
|
||||
public function login(string $username, string $password, bool|Connection $connection): bool
|
||||
{
|
||||
return ftp_login($connection, $username, $password);
|
||||
}
|
||||
|
||||
public function close(bool|Connection $connection): void
|
||||
{
|
||||
ftp_close($connection);
|
||||
}
|
||||
|
||||
public function passive(bool|Connection $connection, bool $passive): bool
|
||||
{
|
||||
return ftp_pasv($connection, $passive);
|
||||
}
|
||||
|
||||
public function delete(bool|Connection $connection, string $path): bool
|
||||
{
|
||||
return ftp_delete($connection, $path);
|
||||
}
|
||||
}
|
90
app/Http/Controllers/Settings/TagController.php
Normal file
90
app/Http/Controllers/Settings/TagController.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Actions\Tag\AttachTag;
|
||||
use App\Actions\Tag\CreateTag;
|
||||
use App\Actions\Tag\DeleteTag;
|
||||
use App\Actions\Tag\DetachTag;
|
||||
use App\Actions\Tag\EditTag;
|
||||
use App\Facades\Toast;
|
||||
use App\Helpers\HtmxResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Tag;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TagController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$data = [
|
||||
'tags' => Tag::getByProjectId(auth()->user()->current_project_id)->get(),
|
||||
];
|
||||
|
||||
if ($request->has('edit')) {
|
||||
$data['editTag'] = Tag::find($request->input('edit'));
|
||||
}
|
||||
|
||||
return view('settings.tags.index', $data);
|
||||
}
|
||||
|
||||
public function create(Request $request): HtmxResponse
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $request->user();
|
||||
|
||||
app(CreateTag::class)->create(
|
||||
$user,
|
||||
$request->input(),
|
||||
);
|
||||
|
||||
Toast::success('Tag created.');
|
||||
|
||||
return htmx()->redirect(route('settings.tags'));
|
||||
}
|
||||
|
||||
public function update(Tag $tag, Request $request): HtmxResponse
|
||||
{
|
||||
app(EditTag::class)->edit(
|
||||
$tag,
|
||||
$request->input(),
|
||||
);
|
||||
|
||||
Toast::success('Tag updated.');
|
||||
|
||||
return htmx()->redirect(route('settings.tags'));
|
||||
}
|
||||
|
||||
public function attach(Request $request): RedirectResponse
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $request->user();
|
||||
|
||||
app(AttachTag::class)->attach($user, $request->input());
|
||||
|
||||
return back()->with([
|
||||
'status' => 'tag-created',
|
||||
]);
|
||||
}
|
||||
|
||||
public function detach(Request $request, Tag $tag): RedirectResponse
|
||||
{
|
||||
app(DetachTag::class)->detach($tag, $request->input());
|
||||
|
||||
return back()->with([
|
||||
'status' => 'tag-detached',
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(Tag $tag): RedirectResponse
|
||||
{
|
||||
app(DeleteTag::class)->delete($tag);
|
||||
|
||||
Toast::success('Tag deleted.');
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
@ -65,4 +65,9 @@ public function sourceControls(): HasMany
|
||||
{
|
||||
return $this->hasMany(SourceControl::class);
|
||||
}
|
||||
|
||||
public function tags(): HasMany
|
||||
{
|
||||
return $this->hasMany(Tag::class);
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@ -214,6 +215,11 @@ public function sshKeys(): BelongsToMany
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function tags(): MorphToMany
|
||||
{
|
||||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
}
|
||||
|
||||
public function getSshUser(): string
|
||||
{
|
||||
if ($this->ssh_user) {
|
||||
|
@ -11,6 +11,7 @@
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
@ -126,6 +127,11 @@ public function ssls(): HasMany
|
||||
return $this->hasMany(Ssl::class);
|
||||
}
|
||||
|
||||
public function tags(): MorphToMany
|
||||
{
|
||||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SourceControlIsNotConnected
|
||||
*/
|
||||
|
55
app/Models/Tag.php
Normal file
55
app/Models/Tag.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $project_id
|
||||
* @property string $name
|
||||
* @property string $color
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
*/
|
||||
class Tag extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'project_id',
|
||||
'name',
|
||||
'color',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'project_id' => 'int',
|
||||
];
|
||||
|
||||
public function project(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
|
||||
public function servers(): MorphToMany
|
||||
{
|
||||
return $this->morphedByMany(Server::class, 'taggable');
|
||||
}
|
||||
|
||||
public function sites(): MorphToMany
|
||||
{
|
||||
return $this->morphedByMany(Site::class, 'taggable');
|
||||
}
|
||||
|
||||
public static function getByProjectId(int $projectId): Builder
|
||||
{
|
||||
return self::query()
|
||||
->where('project_id', $projectId)
|
||||
->orWhereNull('project_id');
|
||||
}
|
||||
}
|
@ -56,7 +56,7 @@ private function checkConnection(string $subject, string $text): bool
|
||||
'content' => '*'.$subject.'*'."\n".$text,
|
||||
]);
|
||||
|
||||
return $connect->ok();
|
||||
return $connect->successful();
|
||||
}
|
||||
|
||||
public function send(object $notifiable, NotificationInterface $notification): void
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Helpers\FTP;
|
||||
use App\Helpers\Notifier;
|
||||
use App\Helpers\SSH;
|
||||
use App\Helpers\Toast;
|
||||
@ -36,5 +37,8 @@ public function boot(): void
|
||||
$this->app->bind('toast', function () {
|
||||
return new Toast;
|
||||
});
|
||||
$this->app->bind('ftp', function () {
|
||||
return new FTP;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
20
app/SSH/HasS3Storage.php
Normal file
20
app/SSH/HasS3Storage.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\SSH;
|
||||
|
||||
trait HasS3Storage
|
||||
{
|
||||
private function prepareS3Path(string $path, string $prefix = ''): string
|
||||
{
|
||||
$path = trim($path);
|
||||
$path = ltrim($path, '/');
|
||||
$path = preg_replace('/[^a-zA-Z0-9\-_\.\/]/', '_', $path);
|
||||
$path = preg_replace('/\/+/', '/', $path);
|
||||
|
||||
if ($prefix) {
|
||||
$path = trim($prefix, '/').'/'.$path;
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common curl zip unzip git gcc openssl
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common curl zip unzip git gcc openssl ufw
|
||||
git config --global user.email "__email__"
|
||||
git config --global user.name "__name__"
|
||||
|
||||
# Install Node.js
|
||||
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -;
|
||||
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get update
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install nodejs -y
|
||||
|
@ -117,6 +117,7 @@ public function deleteUser(string $username, string $host): void
|
||||
public function link(string $username, string $host, array $databases): void
|
||||
{
|
||||
$ssh = $this->service->server->ssh();
|
||||
$version = $this->service->version;
|
||||
|
||||
foreach ($databases as $database) {
|
||||
$ssh->exec(
|
||||
@ -124,6 +125,7 @@ public function link(string $username, string $host, array $databases): void
|
||||
'username' => $username,
|
||||
'host' => $host,
|
||||
'database' => $database,
|
||||
'version' => $version,
|
||||
]),
|
||||
'link-user-to-database'
|
||||
);
|
||||
@ -132,10 +134,13 @@ public function link(string $username, string $host, array $databases): void
|
||||
|
||||
public function unlink(string $username, string $host): void
|
||||
{
|
||||
$version = $this->service->version;
|
||||
|
||||
$this->service->server->ssh()->exec(
|
||||
$this->getScript($this->getScriptsDir().'/unlink.sh', [
|
||||
'username' => $username,
|
||||
'host' => $host,
|
||||
'version' => $version,
|
||||
]),
|
||||
'unlink-user-from-databases'
|
||||
);
|
||||
|
@ -1,5 +1,16 @@
|
||||
if ! sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE __database__ TO __username__;"; then
|
||||
USER_TO_LINK='__username__'
|
||||
DB_NAME='__database__'
|
||||
DB_VERSION='__version__'
|
||||
|
||||
if ! sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE \"$DB_NAME\" TO $USER_TO_LINK;"; then
|
||||
echo 'VITO_SSH_ERROR' && exit 1
|
||||
fi
|
||||
|
||||
echo "Linking to __database__ finished"
|
||||
# Check if PostgreSQL version is 15 or greater
|
||||
if [ "$DB_VERSION" -ge 15 ]; then
|
||||
if ! sudo -u postgres psql -d "$DB_NAME" -c "GRANT USAGE, CREATE ON SCHEMA public TO $USER_TO_LINK;"; then
|
||||
echo 'VITO_SSH_ERROR' && exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Linking to $DB_NAME finished"
|
||||
|
@ -1,10 +1,16 @@
|
||||
USER_TO_REVOKE='__username__'
|
||||
DB_VERSION='__version__'
|
||||
|
||||
DATABASES=$(sudo -u postgres psql -t -c "SELECT datname FROM pg_database WHERE datistemplate = false;")
|
||||
|
||||
for DB in $DATABASES; do
|
||||
echo "Revoking privileges in database: $DB"
|
||||
sudo -u postgres psql -d "$DB" -c "REVOKE ALL PRIVILEGES ON DATABASE \"$DB\" FROM $USER_TO_REVOKE;"
|
||||
|
||||
# Check if PostgreSQL version is 15 or greater
|
||||
if [ "$DB_VERSION" -ge 15 ]; then
|
||||
sudo -u postgres psql -d "$DB" -c "REVOKE USAGE, CREATE ON SCHEMA public FROM $USER_TO_REVOKE;"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Privileges revoked from $USER_TO_REVOKE"
|
||||
|
78
app/SSH/Storage/S3.php
Normal file
78
app/SSH/Storage/S3.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\SSH\Storage;
|
||||
|
||||
use App\Exceptions\SSHCommandError;
|
||||
use App\Models\Server;
|
||||
use App\Models\StorageProvider;
|
||||
use App\SSH\HasS3Storage;
|
||||
use App\SSH\HasScripts;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class S3 extends S3AbstractStorage
|
||||
{
|
||||
use HasS3Storage, HasScripts;
|
||||
|
||||
public function __construct(Server $server, StorageProvider $storageProvider)
|
||||
{
|
||||
parent::__construct($server, $storageProvider);
|
||||
$this->setBucketRegion($this->storageProvider->credentials['region']);
|
||||
$this->setApiUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SSHCommandError
|
||||
*/
|
||||
public function upload(string $src, string $dest): array
|
||||
{
|
||||
$uploadCommand = $this->getScript('s3/upload.sh', [
|
||||
'src' => $src,
|
||||
'bucket' => $this->storageProvider->credentials['bucket'],
|
||||
'dest' => $this->prepareS3Path($this->storageProvider->credentials['path'].'/'.$dest),
|
||||
'key' => $this->storageProvider->credentials['key'],
|
||||
'secret' => $this->storageProvider->credentials['secret'],
|
||||
'region' => $this->getBucketRegion(),
|
||||
'endpoint' => $this->getApiUrl(),
|
||||
]);
|
||||
|
||||
$upload = $this->server->ssh()->exec($uploadCommand, 'upload-to-s3');
|
||||
|
||||
if (str_contains($upload, 'Error') || ! str_contains($upload, 'upload:')) {
|
||||
Log::error('Failed to upload to S3', ['output' => $upload]);
|
||||
throw new SSHCommandError('Failed to upload to S3: '.$upload);
|
||||
}
|
||||
|
||||
return [
|
||||
'size' => null, // You can parse the size from the output if needed
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SSHCommandError
|
||||
*/
|
||||
public function download(string $src, string $dest): void
|
||||
{
|
||||
$downloadCommand = $this->getScript('s3/download.sh', [
|
||||
'src' => $this->prepareS3Path($this->storageProvider->credentials['path'].'/'.$src),
|
||||
'dest' => $dest,
|
||||
'bucket' => $this->storageProvider->credentials['bucket'],
|
||||
'key' => $this->storageProvider->credentials['key'],
|
||||
'secret' => $this->storageProvider->credentials['secret'],
|
||||
'region' => $this->getBucketRegion(),
|
||||
'endpoint' => $this->getApiUrl(),
|
||||
]);
|
||||
|
||||
$download = $this->server->ssh()->exec($downloadCommand, 'download-from-s3');
|
||||
|
||||
if (! str_contains($download, 'Download successful')) {
|
||||
Log::error('Failed to download from S3', ['output' => $download]);
|
||||
throw new SSHCommandError('Failed to download from S3: '.$download);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO Implement delete method
|
||||
*/
|
||||
public function delete(string $path): void {}
|
||||
}
|
32
app/SSH/Storage/S3AbstractStorage.php
Normal file
32
app/SSH/Storage/S3AbstractStorage.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\SSH\Storage;
|
||||
|
||||
abstract class S3AbstractStorage extends AbstractStorage
|
||||
{
|
||||
protected ?string $apiUrl = null;
|
||||
|
||||
protected ?string $bucketRegion = null;
|
||||
|
||||
public function getApiUrl(): string
|
||||
{
|
||||
return $this->apiUrl;
|
||||
}
|
||||
|
||||
public function setApiUrl(?string $region = null): void
|
||||
{
|
||||
$this->bucketRegion = $region ?? $this->bucketRegion;
|
||||
$this->apiUrl = "https://s3.{$this->bucketRegion}.amazonaws.com";
|
||||
}
|
||||
|
||||
// Getter and Setter for $bucketRegion
|
||||
public function getBucketRegion(): string
|
||||
{
|
||||
return $this->bucketRegion;
|
||||
}
|
||||
|
||||
public function setBucketRegion(string $region): void
|
||||
{
|
||||
$this->bucketRegion = $region;
|
||||
}
|
||||
}
|
84
app/SSH/Storage/Wasabi.php
Normal file
84
app/SSH/Storage/Wasabi.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\SSH\Storage;
|
||||
|
||||
use App\Exceptions\SSHCommandError;
|
||||
use App\Models\Server;
|
||||
use App\Models\StorageProvider;
|
||||
use App\SSH\HasS3Storage;
|
||||
use App\SSH\HasScripts;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Wasabi extends S3AbstractStorage
|
||||
{
|
||||
use HasS3Storage, HasScripts;
|
||||
|
||||
public function __construct(Server $server, StorageProvider $storageProvider)
|
||||
{
|
||||
parent::__construct($server, $storageProvider);
|
||||
$this->setBucketRegion($this->storageProvider->credentials['region']);
|
||||
$this->setApiUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SSHCommandError
|
||||
*/
|
||||
public function upload(string $src, string $dest): array
|
||||
{
|
||||
$uploadCommand = $this->getScript('wasabi/upload.sh', [
|
||||
'src' => $src,
|
||||
'bucket' => $this->storageProvider->credentials['bucket'],
|
||||
'dest' => $this->prepareS3Path($this->storageProvider->credentials['path'].'/'.$dest),
|
||||
'key' => $this->storageProvider->credentials['key'],
|
||||
'secret' => $this->storageProvider->credentials['secret'],
|
||||
'region' => $this->storageProvider->credentials['region'],
|
||||
'endpoint' => $this->getApiUrl(),
|
||||
]);
|
||||
|
||||
$upload = $this->server->ssh()->exec($uploadCommand, 'upload-to-wasabi');
|
||||
|
||||
if (str_contains($upload, 'Error') || ! str_contains($upload, 'upload:')) {
|
||||
Log::error('Failed to upload to wasabi', ['output' => $upload]);
|
||||
throw new SSHCommandError('Failed to upload to wasabi: '.$upload);
|
||||
}
|
||||
|
||||
return [
|
||||
'size' => null, // You can parse the size from the output if needed
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SSHCommandError
|
||||
*/
|
||||
public function download(string $src, string $dest): void
|
||||
{
|
||||
$downloadCommand = $this->getScript('wasabi/download.sh', [
|
||||
'src' => $this->prepareS3Path($this->storageProvider->credentials['path'].'/'.$src),
|
||||
'dest' => $dest,
|
||||
'bucket' => $this->storageProvider->credentials['bucket'],
|
||||
'key' => $this->storageProvider->credentials['key'],
|
||||
'secret' => $this->storageProvider->credentials['secret'],
|
||||
'region' => $this->storageProvider->credentials['region'],
|
||||
'endpoint' => $this->getApiUrl(),
|
||||
]);
|
||||
|
||||
$download = $this->server->ssh()->exec($downloadCommand, 'download-from-wasabi');
|
||||
|
||||
if (! str_contains($download, 'Download successful')) {
|
||||
Log::error('Failed to download from wasabi', ['output' => $download]);
|
||||
throw new SSHCommandError('Failed to download from wasabi: '.$download);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO Implement delete method
|
||||
*/
|
||||
public function delete(string $path): void {}
|
||||
|
||||
public function setApiUrl(?string $region = null): void
|
||||
{
|
||||
$this->bucketRegion = $region ?? $this->bucketRegion;
|
||||
$this->apiUrl = "https://{$this->storageProvider->credentials['bucket']}.s3.{$this->getBucketRegion()}.wasabisys.com";
|
||||
}
|
||||
}
|
32
app/SSH/Storage/scripts/s3/download.sh
Normal file
32
app/SSH/Storage/scripts/s3/download.sh
Normal file
@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Configure AWS CLI with provided credentials
|
||||
/usr/local/bin/aws configure set aws_access_key_id "__key__"
|
||||
/usr/local/bin/aws configure set aws_secret_access_key "__secret__"
|
||||
/usr/local/bin/aws configure set default.region "__region__"
|
||||
|
||||
# Use the provided endpoint in the correct format
|
||||
ENDPOINT="__endpoint__"
|
||||
BUCKET="__bucket__"
|
||||
REGION="__region__"
|
||||
|
||||
# Ensure that DEST does not have a trailing slash
|
||||
SRC="__src__"
|
||||
DEST="__dest__"
|
||||
|
||||
# Download the file from S3
|
||||
echo "Downloading s3://__bucket__/__src__ to __dest__"
|
||||
download_output=$(/usr/local/bin/aws s3 cp "s3://$BUCKET/$SRC" "$DEST" --endpoint-url="$ENDPOINT" --region "$REGION" 2>&1)
|
||||
download_exit_code=$?
|
||||
|
||||
# Log output and exit code
|
||||
echo "Download command output: $download_output"
|
||||
echo "Download command exit code: $download_exit_code"
|
||||
|
||||
# Check if the download was successful
|
||||
if [ $download_exit_code -eq 0 ]; then
|
||||
echo "Download successful"
|
||||
else
|
||||
echo "Download failed"
|
||||
exit 1
|
||||
fi
|
59
app/SSH/Storage/scripts/s3/upload.sh
Normal file
59
app/SSH/Storage/scripts/s3/upload.sh
Normal file
@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check if AWS CLI is installed
|
||||
if ! command -v aws &> /dev/null
|
||||
then
|
||||
echo "AWS CLI is not installed. Installing..."
|
||||
|
||||
# Detect system architecture
|
||||
ARCH=$(uname -m)
|
||||
if [ "$ARCH" == "x86_64" ]; then
|
||||
CLI_URL="https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"
|
||||
elif [ "$ARCH" == "aarch64" ]; then
|
||||
CLI_URL="https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip"
|
||||
else
|
||||
echo "Unsupported architecture: $ARCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download and install AWS CLI
|
||||
sudo curl "$CLI_URL" -o "awscliv2.zip"
|
||||
sudo unzip awscliv2.zip
|
||||
sudo ./aws/install --update
|
||||
sudo rm -rf awscliv2.zip aws
|
||||
|
||||
echo "AWS CLI installation completed."
|
||||
else
|
||||
echo "AWS CLI is already installed."
|
||||
/usr/local/bin/aws --version
|
||||
fi
|
||||
|
||||
# Configure AWS CLI with provided credentials
|
||||
/usr/local/bin/aws configure set aws_access_key_id "__key__"
|
||||
/usr/local/bin/aws configure set aws_secret_access_key "__secret__"
|
||||
|
||||
# Use the provided endpoint in the correct format
|
||||
ENDPOINT="__endpoint__"
|
||||
BUCKET="__bucket__"
|
||||
REGION="__region__"
|
||||
|
||||
# Ensure that DEST does not have a trailing slash
|
||||
SRC="__src__"
|
||||
DEST="__dest__"
|
||||
|
||||
# Upload the file
|
||||
echo "Uploading __src__ to s3://$BUCKET/$DEST"
|
||||
upload_output=$(/usr/local/bin/aws s3 cp "$SRC" "s3://$BUCKET/$DEST" --endpoint-url="$ENDPOINT" --region "$REGION" 2>&1)
|
||||
upload_exit_code=$?
|
||||
|
||||
# Log output and exit code
|
||||
echo "Upload command output: $upload_output"
|
||||
echo "Upload command exit code: $upload_exit_code"
|
||||
|
||||
# Check if the upload was successful
|
||||
if [ $upload_exit_code -eq 0 ]; then
|
||||
echo "Upload successful"
|
||||
else
|
||||
echo "Upload failed"
|
||||
exit 1
|
||||
fi
|
31
app/SSH/Storage/scripts/wasabi/download.sh
Normal file
31
app/SSH/Storage/scripts/wasabi/download.sh
Normal file
@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Configure AWS CLI with provided credentials
|
||||
/usr/local/bin/aws configure set aws_access_key_id "__key__"
|
||||
/usr/local/bin/aws configure set aws_secret_access_key "__secret__"
|
||||
|
||||
# Use the provided endpoint in the correct format
|
||||
ENDPOINT="__endpoint__"
|
||||
BUCKET="__bucket__"
|
||||
REGION="__region__"
|
||||
|
||||
# Ensure that DEST does not have a trailing slash
|
||||
SRC="__src__"
|
||||
DEST="__dest__"
|
||||
|
||||
# Download the file from S3
|
||||
echo "Downloading s3://__bucket____src__ to __dest__"
|
||||
download_output=$(/usr/local/bin/aws s3 cp "s3://$BUCKET/$SRC" "$DEST" --endpoint-url="$ENDPOINT" --region "$REGION" 2>&1)
|
||||
download_exit_code=$?
|
||||
|
||||
# Log output and exit code
|
||||
echo "Download command output: $download_output"
|
||||
echo "Download command exit code: $download_exit_code"
|
||||
|
||||
# Check if the download was successful
|
||||
if [ $download_exit_code -eq 0 ]; then
|
||||
echo "Download successful"
|
||||
else
|
||||
echo "Download failed"
|
||||
exit 1
|
||||
fi
|
59
app/SSH/Storage/scripts/wasabi/upload.sh
Normal file
59
app/SSH/Storage/scripts/wasabi/upload.sh
Normal file
@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check if AWS CLI is installed
|
||||
if ! command -v aws &> /dev/null
|
||||
then
|
||||
echo "AWS CLI is not installed. Installing..."
|
||||
|
||||
# Detect system architecture
|
||||
ARCH=$(uname -m)
|
||||
if [ "$ARCH" == "x86_64" ]; then
|
||||
CLI_URL="https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"
|
||||
elif [ "$ARCH" == "aarch64" ]; then
|
||||
CLI_URL="https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip"
|
||||
else
|
||||
echo "Unsupported architecture: $ARCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download and install AWS CLI
|
||||
sudo curl "$CLI_URL" -o "awscliv2.zip"
|
||||
sudo unzip awscliv2.zip
|
||||
sudo ./aws/install --update
|
||||
sudo rm -rf awscliv2.zip aws
|
||||
|
||||
echo "AWS CLI installation completed."
|
||||
else
|
||||
echo "AWS CLI is already installed."
|
||||
aws --version
|
||||
fi
|
||||
|
||||
# Configure AWS CLI with provided credentials
|
||||
/usr/local/bin/aws configure set aws_access_key_id "__key__"
|
||||
/usr/local/bin/aws configure set aws_secret_access_key "__secret__"
|
||||
|
||||
# Use the provided endpoint in the correct format
|
||||
ENDPOINT="__endpoint__"
|
||||
BUCKET="__bucket__"
|
||||
REGION="__region__"
|
||||
|
||||
# Ensure that DEST does not have a trailing slash
|
||||
SRC="__src__"
|
||||
DEST="__dest__"
|
||||
|
||||
# Upload the file
|
||||
echo "Uploading __src__ to s3://$BUCKET/$DEST"
|
||||
upload_output=$(/usr/local/bin/aws s3 cp "$SRC" "s3://$BUCKET/$DEST" --endpoint-url="$ENDPOINT" --region "$REGION" 2>&1)
|
||||
upload_exit_code=$?
|
||||
|
||||
# Log output and exit code
|
||||
echo "Upload command output: $upload_output"
|
||||
echo "Upload command exit code: $upload_exit_code"
|
||||
|
||||
# Check if the upload was successful
|
||||
if [ $upload_exit_code -eq 0 ]; then
|
||||
echo "Upload successful"
|
||||
else
|
||||
echo "Upload failed"
|
||||
exit 1
|
||||
fi
|
@ -41,7 +41,7 @@ public function connect(): bool
|
||||
$isConnected = $connection && $this->login($connection);
|
||||
|
||||
if ($isConnected) {
|
||||
ftp_close($connection);
|
||||
\App\Facades\FTP::close($connection);
|
||||
}
|
||||
|
||||
return $isConnected;
|
||||
@ -58,31 +58,36 @@ public function delete(array $paths): void
|
||||
|
||||
if ($connection && $this->login($connection)) {
|
||||
if ($this->storageProvider->credentials['passive']) {
|
||||
ftp_pasv($connection, true);
|
||||
\App\Facades\FTP::passive($connection, true);
|
||||
}
|
||||
|
||||
foreach ($paths as $path) {
|
||||
ftp_delete($connection, $this->storageProvider->credentials['path'].'/'.$path);
|
||||
\App\Facades\FTP::delete($connection, $this->storageProvider->credentials['path'].'/'.$path);
|
||||
}
|
||||
}
|
||||
|
||||
ftp_close($connection);
|
||||
\App\Facades\FTP::close($connection);
|
||||
}
|
||||
|
||||
private function connection(): bool|Connection
|
||||
{
|
||||
$credentials = $this->storageProvider->credentials;
|
||||
if ($credentials['ssl']) {
|
||||
return ftp_ssl_connect($credentials['host'], $credentials['port'], 5);
|
||||
}
|
||||
|
||||
return ftp_connect($credentials['host'], $credentials['port'], 5);
|
||||
return \App\Facades\FTP::connect(
|
||||
$credentials['host'],
|
||||
$credentials['port'],
|
||||
$credentials['ssl']
|
||||
);
|
||||
}
|
||||
|
||||
private function login(Connection $connection): bool
|
||||
private function login(bool|Connection $connection): bool
|
||||
{
|
||||
$credentials = $this->storageProvider->credentials;
|
||||
|
||||
return ftp_login($connection, $credentials['username'], $credentials['password']);
|
||||
return \App\Facades\FTP::login(
|
||||
$credentials['username'],
|
||||
$credentials['password'],
|
||||
$connection
|
||||
);
|
||||
}
|
||||
}
|
||||
|
57
app/StorageProviders/S3.php
Normal file
57
app/StorageProviders/S3.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\StorageProviders;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\SSH\Storage\S3 as S3Storage;
|
||||
use App\SSH\Storage\Storage;
|
||||
use Aws\S3\Exception\S3Exception;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class S3 extends S3AbstractStorageProvider
|
||||
{
|
||||
public function validationRules(): array
|
||||
{
|
||||
return [
|
||||
'key' => 'required|string',
|
||||
'secret' => 'required|string',
|
||||
'region' => 'required|string',
|
||||
'bucket' => 'required|string',
|
||||
'path' => 'required|string',
|
||||
];
|
||||
}
|
||||
|
||||
public function credentialData(array $input): array
|
||||
{
|
||||
return [
|
||||
'key' => $input['key'],
|
||||
'secret' => $input['secret'],
|
||||
'region' => $input['region'],
|
||||
'bucket' => $input['bucket'],
|
||||
'path' => $input['path'],
|
||||
];
|
||||
}
|
||||
|
||||
public function connect(): bool
|
||||
{
|
||||
try {
|
||||
$this->setBucketRegion($this->storageProvider->credentials['region']);
|
||||
$this->setApiUrl();
|
||||
$this->buildClientConfig();
|
||||
$this->getClient()->listBuckets();
|
||||
|
||||
return true;
|
||||
} catch (S3Exception $e) {
|
||||
Log::error('Failed to connect to S3', ['exception' => $e]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function ssh(Server $server): Storage
|
||||
{
|
||||
return new S3Storage($server, $this->storageProvider);
|
||||
}
|
||||
|
||||
public function delete(array $paths): void {}
|
||||
}
|
74
app/StorageProviders/S3AbstractStorageProvider.php
Normal file
74
app/StorageProviders/S3AbstractStorageProvider.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\StorageProviders;
|
||||
|
||||
use App\Models\StorageProvider;
|
||||
use Aws\S3\S3Client;
|
||||
|
||||
abstract class S3AbstractStorageProvider extends AbstractStorageProvider implements S3ClientInterface, S3StorageInterface
|
||||
{
|
||||
protected ?string $apiUrl = null;
|
||||
|
||||
protected ?string $bucketRegion = null;
|
||||
|
||||
protected ?S3Client $client = null;
|
||||
|
||||
protected StorageProvider $storageProvider;
|
||||
|
||||
protected array $clientConfig = [];
|
||||
|
||||
public function getApiUrl(): string
|
||||
{
|
||||
return $this->apiUrl;
|
||||
}
|
||||
|
||||
public function setApiUrl(?string $region = null): void
|
||||
{
|
||||
$this->bucketRegion = $region ?? $this->bucketRegion;
|
||||
$this->apiUrl = "https://s3.{$this->bucketRegion}.amazonaws.com";
|
||||
}
|
||||
|
||||
public function getBucketRegion(): string
|
||||
{
|
||||
return $this->bucketRegion;
|
||||
}
|
||||
|
||||
public function setBucketRegion(string $region): void
|
||||
{
|
||||
$this->bucketRegion = $region;
|
||||
}
|
||||
|
||||
public function getClient(): S3Client
|
||||
{
|
||||
return new S3Client($this->clientConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the configuration array for the S3 client.
|
||||
* This method can be overridden by child classes to modify the configuration.
|
||||
*/
|
||||
public function buildClientConfig(): array
|
||||
{
|
||||
$this->clientConfig = [
|
||||
'credentials' => [
|
||||
'key' => $this->storageProvider->credentials['key'],
|
||||
'secret' => $this->storageProvider->credentials['secret'],
|
||||
],
|
||||
'region' => $this->getBucketRegion(),
|
||||
'version' => 'latest',
|
||||
'endpoint' => $this->getApiUrl(),
|
||||
];
|
||||
|
||||
return $this->clientConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or update a configuration parameter for the S3 client.
|
||||
*/
|
||||
public function setConfigParam(array $param): void
|
||||
{
|
||||
foreach ($param as $key => $value) {
|
||||
$this->clientConfig[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
10
app/StorageProviders/S3ClientInterface.php
Normal file
10
app/StorageProviders/S3ClientInterface.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\StorageProviders;
|
||||
|
||||
use Aws\S3\S3Client;
|
||||
|
||||
interface S3ClientInterface
|
||||
{
|
||||
public function getClient(): S3Client;
|
||||
}
|
14
app/StorageProviders/S3StorageInterface.php
Normal file
14
app/StorageProviders/S3StorageInterface.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\StorageProviders;
|
||||
|
||||
interface S3StorageInterface
|
||||
{
|
||||
public function getApiUrl(): string;
|
||||
|
||||
public function setApiUrl(?string $region = null): void;
|
||||
|
||||
public function getBucketRegion(): string;
|
||||
|
||||
public function setBucketRegion(string $region): void;
|
||||
}
|
85
app/StorageProviders/Wasabi.php
Normal file
85
app/StorageProviders/Wasabi.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\StorageProviders;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\SSH\Storage\Storage;
|
||||
use App\SSH\Storage\Wasabi as WasabiStorage;
|
||||
use Aws\S3\Exception\S3Exception;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Wasabi extends S3AbstractStorageProvider
|
||||
{
|
||||
private const DEFAULT_REGION = 'us-east-1';
|
||||
|
||||
public function validationRules(): array
|
||||
{
|
||||
return [
|
||||
'key' => 'required|string',
|
||||
'secret' => 'required|string',
|
||||
'region' => 'required|string',
|
||||
'bucket' => 'required|string',
|
||||
'path' => 'required|string',
|
||||
];
|
||||
}
|
||||
|
||||
public function credentialData(array $input): array
|
||||
{
|
||||
return [
|
||||
'key' => $input['key'],
|
||||
'secret' => $input['secret'],
|
||||
'region' => $input['region'],
|
||||
'bucket' => $input['bucket'],
|
||||
'path' => $input['path'],
|
||||
];
|
||||
}
|
||||
|
||||
public function connect(): bool
|
||||
{
|
||||
try {
|
||||
$this->setBucketRegion(self::DEFAULT_REGION);
|
||||
$this->setApiUrl();
|
||||
$this->buildClientConfig();
|
||||
$this->getClient()->listBuckets();
|
||||
|
||||
return true;
|
||||
} catch (S3Exception $e) {
|
||||
Log::error('Failed to connect to S3', ['exception' => $e]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the configuration array for the S3 client.
|
||||
* This method can be overridden by child classes to modify the configuration.
|
||||
*/
|
||||
public function buildClientConfig(): array
|
||||
{
|
||||
$this->clientConfig = [
|
||||
'credentials' => [
|
||||
'key' => $this->storageProvider->credentials['key'],
|
||||
'secret' => $this->storageProvider->credentials['secret'],
|
||||
],
|
||||
'region' => $this->getBucketRegion(),
|
||||
'version' => 'latest',
|
||||
'endpoint' => $this->getApiUrl(),
|
||||
'use_path_style_endpoint' => true,
|
||||
];
|
||||
|
||||
return $this->clientConfig;
|
||||
}
|
||||
|
||||
public function ssh(Server $server): Storage
|
||||
{
|
||||
return new WasabiStorage($server, $this->storageProvider);
|
||||
}
|
||||
|
||||
public function setApiUrl(?string $region = null): void
|
||||
{
|
||||
$this->bucketRegion = $region ?? $this->bucketRegion;
|
||||
$this->apiUrl = "https://s3.{$this->bucketRegion}.wasabisys.com";
|
||||
}
|
||||
|
||||
public function delete(array $paths): void {}
|
||||
}
|
60
app/Support/Testing/FTPFake.php
Normal file
60
app/Support/Testing/FTPFake.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Testing;
|
||||
|
||||
use FTP\Connection;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class FTPFake
|
||||
{
|
||||
protected array $connections = [];
|
||||
|
||||
protected array $logins = [];
|
||||
|
||||
public function connect(string $host, string $port, bool $ssl = false): bool|Connection
|
||||
{
|
||||
$this->connections[] = compact('host', 'port', 'ssl');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function login(string $username, string $password, bool|Connection $connection): bool
|
||||
{
|
||||
$this->logins[] = compact('username', 'password');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function close(bool|Connection $connection): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function passive(bool|Connection $connection, bool $passive): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function delete(bool|Connection $connection, string $path): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function assertConnected(string $host): void
|
||||
{
|
||||
if (! $this->connections) {
|
||||
Assert::fail('No connections are made');
|
||||
}
|
||||
$connected = false;
|
||||
foreach ($this->connections as $connection) {
|
||||
if ($connection['host'] === $host) {
|
||||
$connected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (! $connected) {
|
||||
Assert::fail('The expected host is not connected');
|
||||
}
|
||||
Assert::assertTrue(true, $connected);
|
||||
}
|
||||
}
|
47
app/View/Components/Editor.php
Normal file
47
app/View/Components/Editor.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Editor extends Component
|
||||
{
|
||||
public string $id;
|
||||
|
||||
public string $name;
|
||||
|
||||
public ?string $value;
|
||||
|
||||
public array $options;
|
||||
|
||||
public function __construct(
|
||||
string $name,
|
||||
?string $value,
|
||||
public string $lang,
|
||||
public bool $readonly = false,
|
||||
public bool $lineNumbers = true,
|
||||
) {
|
||||
$this->id = $name.'-'.Str::random(8);
|
||||
$this->name = $name;
|
||||
$this->value = json_encode($value ?? '');
|
||||
$this->options = $this->getOptions();
|
||||
}
|
||||
|
||||
private function getOptions(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'lang' => $this->lang,
|
||||
'value' => $this->value,
|
||||
];
|
||||
}
|
||||
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.editor');
|
||||
}
|
||||
}
|
727
composer.lock
generated
727
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -427,11 +427,16 @@
|
||||
\App\Enums\StorageProvider::DROPBOX,
|
||||
\App\Enums\StorageProvider::FTP,
|
||||
\App\Enums\StorageProvider::LOCAL,
|
||||
\App\Enums\StorageProvider::S3,
|
||||
\App\Enums\StorageProvider::WASABI,
|
||||
],
|
||||
'storage_providers_class' => [
|
||||
\App\Enums\StorageProvider::DROPBOX => \App\StorageProviders\Dropbox::class,
|
||||
\App\Enums\StorageProvider::FTP => \App\StorageProviders\Ftp::class,
|
||||
\App\Enums\StorageProvider::FTP => \App\StorageProviders\FTP::class,
|
||||
\App\Enums\StorageProvider::LOCAL => \App\StorageProviders\Local::class,
|
||||
\App\Enums\StorageProvider::S3 => \App\StorageProviders\S3::class,
|
||||
\App\Enums\StorageProvider::WASABI => \App\StorageProviders\Wasabi::class,
|
||||
|
||||
],
|
||||
|
||||
'ssl_types' => [
|
||||
@ -445,4 +450,30 @@
|
||||
30,
|
||||
90,
|
||||
],
|
||||
|
||||
'tag_colors' => [
|
||||
'slate',
|
||||
'gray',
|
||||
'red',
|
||||
'orange',
|
||||
'amber',
|
||||
'yellow',
|
||||
'lime',
|
||||
'green',
|
||||
'emerald',
|
||||
'teal',
|
||||
'cyan',
|
||||
'sky',
|
||||
'blue',
|
||||
'indigo',
|
||||
'violet',
|
||||
'purple',
|
||||
'fuchsia',
|
||||
'pink',
|
||||
'rose',
|
||||
],
|
||||
'taggable_types' => [
|
||||
\App\Models\Server::class,
|
||||
\App\Models\Site::class,
|
||||
],
|
||||
];
|
||||
|
@ -618,8 +618,8 @@
|
||||
'images' => [
|
||||
'ubuntu_18' => '112929540',
|
||||
'ubuntu_20' => '112929454',
|
||||
'ubuntu_22' => '129211873',
|
||||
'ubuntu_24' => '155133621',
|
||||
'ubuntu_22' => '159651797',
|
||||
'ubuntu_24' => '160232537',
|
||||
],
|
||||
],
|
||||
'vultr' => [
|
||||
|
23
database/factories/TagFactory.php
Normal file
23
database/factories/TagFactory.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class TagFactory extends Factory
|
||||
{
|
||||
protected $model = Tag::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'project_id' => 1,
|
||||
'created_at' => Carbon::now(), //
|
||||
'updated_at' => Carbon::now(),
|
||||
'name' => $this->faker->randomElement(['production', 'staging', 'development']),
|
||||
'color' => $this->faker->randomElement(config('core.tag_colors')),
|
||||
];
|
||||
}
|
||||
}
|
24
database/migrations/2024_08_09_180021_create_tags_table.php
Normal file
24
database/migrations/2024_08_09_180021_create_tags_table.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?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('tags', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('project_id');
|
||||
$table->string('name');
|
||||
$table->string('color');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('tags');
|
||||
}
|
||||
};
|
@ -0,0 +1,24 @@
|
||||
<?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('taggables', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tag_id');
|
||||
$table->unsignedBigInteger('taggable_id');
|
||||
$table->string('taggable_type');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('taggables');
|
||||
}
|
||||
};
|
@ -8,9 +8,12 @@
|
||||
use App\Models\Site;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithFaker;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
@ -20,6 +23,12 @@ public function run(): void
|
||||
'name' => 'Test User',
|
||||
'email' => 'user@example.com',
|
||||
]);
|
||||
|
||||
$this->createResources($user);
|
||||
}
|
||||
|
||||
private function createResources(User $user): void
|
||||
{
|
||||
$server = Server::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'project_id' => $user->currentProject->id,
|
||||
|
30
package-lock.json
generated
30
package-lock.json
generated
@ -4,13 +4,13 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vito",
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"alpinejs": "^3.4.2",
|
||||
"apexcharts": "^3.44.2",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"brace": "^0.11.1",
|
||||
"flowbite": "^2.3.0",
|
||||
"flowbite-datepicker": "^1.2.6",
|
||||
"htmx.org": "^1.9.10",
|
||||
@ -22,7 +22,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"tailwindcss": "^3.1.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"vite": "^4.5.3"
|
||||
"vite": "^4.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@ -685,6 +685,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/brace": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz",
|
||||
"integrity": "sha512-Fc8Ne62jJlKHiG/ajlonC4Sd66Pq68fFwK4ihJGNZpGqboc324SQk+lRvMzpPRuJOmfrJefdG8/7JdWX4bzJ2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
@ -1277,12 +1283,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.2",
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@ -1792,9 +1798,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "3.29.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
|
||||
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
|
||||
"version": "3.29.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
|
||||
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
@ -2228,9 +2234,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
|
||||
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
|
||||
"version": "4.5.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz",
|
||||
"integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
|
@ -10,8 +10,11 @@
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"alpinejs": "^3.4.2",
|
||||
"apexcharts": "^3.44.2",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"brace": "^0.11.1",
|
||||
"flowbite": "^2.3.0",
|
||||
"flowbite-datepicker": "^1.2.6",
|
||||
"htmx.org": "^1.9.10",
|
||||
"laravel-echo": "^1.15.0",
|
||||
"laravel-vite-plugin": "^0.7.2",
|
||||
@ -21,8 +24,6 @@
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"tailwindcss": "^3.1.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"vite": "^4.5.3",
|
||||
"apexcharts": "^3.44.2",
|
||||
"flowbite-datepicker": "^1.2.6"
|
||||
"vite": "^4.5.5"
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
public/build/assets/app-888ea5fa.css
Normal file
1
public/build/assets/app-888ea5fa.css
Normal file
File diff suppressed because one or more lines are too long
807
public/build/assets/app-d9a3bf01.js
Normal file
807
public/build/assets/app-d9a3bf01.js
Normal file
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
{
|
||||
"resources/css/app.css": {
|
||||
"file": "assets/app-7f487305.css",
|
||||
"file": "assets/app-888ea5fa.css",
|
||||
"isEntry": true,
|
||||
"src": "resources/css/app.css"
|
||||
},
|
||||
@ -12,7 +12,7 @@
|
||||
"css": [
|
||||
"assets/app-a1ae07b3.css"
|
||||
],
|
||||
"file": "assets/app-01264060.js",
|
||||
"file": "assets/app-d9a3bf01.js",
|
||||
"isEntry": true,
|
||||
"src": "resources/js/app.js"
|
||||
}
|
||||
|
1
public/static/images/s3.svg
Normal file
1
public/static/images/s3.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><title>AWS Simple Storage Service (S3)</title><defs><linearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="a"><stop stop-color="#1B660F" offset="0%"/><stop stop-color="#6CAE3E" offset="100%"/></linearGradient></defs><g><rect fill="url(#a)" width="256" height="256"/><path d="M194.67488,137.25632 L195.90368,128.60352 C207.23488,135.39072 207.38208,138.19392 207.378964,138.27072 C207.35968,138.28672 205.42688,139.89952 194.67488,137.25632 L194.67488,137.25632 Z M188.45728,135.52832 C168.87328,129.60192 141.59968,117.08992 130.56288,111.87392 C130.56288,111.82912 130.57568,111.78752 130.57568,111.74272 C130.57568,107.50272 127.12608,104.05312 122.88288,104.05312 C118.64608,104.05312 115.19648,107.50272 115.19648,111.74272 C115.19648,115.98272 118.64608,119.43232 122.88288,119.43232 C124.74528,119.43232 126.43488,118.73792 127.76928,117.63392 C140.75488,123.78112 167.81728,136.11072 187.54528,141.93472 L179.74368,196.99392 C179.72128,197.14432 179.71168,197.29472 179.71168,197.44512 C179.71168,202.29312 158.24928,211.19872 123.18048,211.19872 C87.74048,211.19872 66.05088,202.29312 66.05088,197.44512 C66.05088,197.29792 66.04128,197.15392 66.02208,197.00992 L49.72128,77.94752 C63.83008,87.65952 94.17568,92.79872 123.19968,92.79872 C152.17888,92.79872 182.47328,87.67872 196.61088,77.99552 L188.45728,135.52832 Z M47.99968,65.52832 C48.23008,61.31712 72.42848,44.79872 123.19968,44.79872 C173.96448,44.79872 198.16608,61.31392 198.39968,65.52832 L198.39968,66.96512 C195.61568,76.40832 164.25568,86.39872 123.19968,86.39872 C82.07328,86.39872 50.69728,76.37632 47.99968,66.92032 L47.99968,65.52832 Z M204.79968,65.59872 C204.79968,54.51072 173.01088,38.39872 123.19968,38.39872 C73.38848,38.39872 41.59968,54.51072 41.59968,65.59872 L41.90048,68.01152 L59.65408,197.68832 C60.07968,212.19072 98.75488,217.59872 123.18048,217.59872 C153.49088,217.59872 185.69248,210.62912 186.10848,197.69792 L193.77568,143.62752 C198.04128,144.64832 201.55168,145.16992 204.37088,145.16992 C208.15648,145.16992 210.71648,144.24512 212.26848,142.39552 C213.54208,140.87872 214.02848,139.04192 213.66368,137.08672 C212.83488,132.65792 207.57728,127.88352 196.87008,121.77472 L204.47328,68.13632 L204.79968,65.59872 Z" fill="#FFFFFF"/></g></svg>
|
After Width: | Height: | Size: 2.3 KiB |
9
public/static/images/wasabi.svg
Normal file
9
public/static/images/wasabi.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 51 KiB |
40
resources/js/ace-editor/ace-editor.js
Normal file
40
resources/js/ace-editor/ace-editor.js
Normal file
@ -0,0 +1,40 @@
|
||||
import ace from 'brace';
|
||||
import 'brace/mode/javascript';
|
||||
import 'brace/mode/plain_text';
|
||||
import 'brace/mode/sh';
|
||||
import 'brace/mode/ini';
|
||||
import 'brace/ext/searchbox'
|
||||
import './theme-vito'
|
||||
import './mode-env';
|
||||
import './mode-nginx';
|
||||
|
||||
window.initAceEditor = function (options = {}) {
|
||||
const editorValue = JSON.parse(options.value || '');
|
||||
const editor = ace.edit(options.id);
|
||||
editor.setTheme("ace/theme/vito");
|
||||
editor.getSession().setMode(`ace/mode/${options.lang || 'plain_text'}`);
|
||||
editor.setValue(editorValue, -1);
|
||||
editor.clearSelection();
|
||||
editor.focus();
|
||||
editor.setOptions({
|
||||
enableBasicAutocompletion: true,
|
||||
enableSnippets: true,
|
||||
enableLiveAutocompletion: true,
|
||||
printMargin: false,
|
||||
});
|
||||
|
||||
editor.renderer.setScrollMargin(15, 15, 0, 0)
|
||||
editor.renderer.setPadding(15);
|
||||
|
||||
editor.getSession().on('change', function () {
|
||||
document.getElementById(`textarea-${options.id}`).value = editor.getValue();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
editor.resize();
|
||||
})
|
||||
|
||||
document.getElementById(`textarea-${options.id}`).innerHTML = editorValue;
|
||||
|
||||
return editor;
|
||||
}
|
136
resources/js/ace-editor/mode-env.js
Normal file
136
resources/js/ace-editor/mode-env.js
Normal file
@ -0,0 +1,136 @@
|
||||
ace.define("ace/mode/env", ["require", "exports", "module", "ace/lib/oop", "ace/mode/text", "ace/mode/env_highlight_rules", "ace/mode/folding/ini","ace/mode/behaviour"], function (require, exports) {
|
||||
var oop = require("../lib/oop");
|
||||
var TextMode = require("./text").Mode;
|
||||
var Behaviour = require("./behaviour").Behaviour;
|
||||
var envHighlightRules = require("./env_highlight_rules").envHighlightRules;
|
||||
|
||||
var Mode = function () {
|
||||
this.HighlightRules = envHighlightRules;
|
||||
this.$behaviour = new Behaviour
|
||||
};
|
||||
|
||||
oop.inherits(Mode, TextMode);
|
||||
|
||||
(function() {
|
||||
this.lineCommentStart = "#",
|
||||
this.blockComment = null,
|
||||
this.$id = "ace/mode/env"
|
||||
}).call(Mode.prototype),
|
||||
|
||||
exports.Mode = Mode;
|
||||
})
|
||||
ace.define("ace/mode/env_highlight_rules", ["require", "exports", "module", "ace/lib/oop", "ace/mode/text_highlight_rules"], function (require, exports, module) {
|
||||
"use strict";
|
||||
|
||||
var oop = require("../lib/oop");
|
||||
var TextHighlightRules =
|
||||
require("./text_highlight_rules").TextHighlightRules;
|
||||
|
||||
var envHighlightRules = function () {
|
||||
this.$rules = {
|
||||
start: [
|
||||
{
|
||||
token: "punctuation.definition.comment.env",
|
||||
regex: "#.*",
|
||||
push_: [
|
||||
{
|
||||
token: "comment.line.number-sign.env",
|
||||
regex: "$|^",
|
||||
next: "pop",
|
||||
},
|
||||
{
|
||||
defaultToken: "comment.line.number-sign.env",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
token: "punctuation.definition.comment.env",
|
||||
regex: "#.*",
|
||||
push_: [
|
||||
{
|
||||
token: "comment.line.semicolon.env",
|
||||
regex: "$|^",
|
||||
next: "pop",
|
||||
},
|
||||
{
|
||||
defaultToken: "comment.line.semicolon.env",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
token: [
|
||||
"keyword.other.definition.env",
|
||||
"text",
|
||||
"punctuation.separator.key-value.env",
|
||||
],
|
||||
regex: "\\b([a-zA-Z0-9_.-]+)\\b(\\s*)(=)",
|
||||
},
|
||||
{
|
||||
token: [
|
||||
"punctuation.definition.entity.env",
|
||||
"constant.section.group-title.env",
|
||||
"punctuation.definition.entity.env",
|
||||
],
|
||||
regex: "^(\\[)(.*?)(\\])",
|
||||
},
|
||||
{
|
||||
token: "punctuation.definition.string.begin.env",
|
||||
regex: "'",
|
||||
push: [
|
||||
{
|
||||
token: "punctuation.definition.string.end.env",
|
||||
regex: "'",
|
||||
next: "pop",
|
||||
},
|
||||
{
|
||||
token: "constant.language.escape",
|
||||
regex: "\\\\(?:[\\\\0abtrn;#=:]|x[a-fA-F\\d]{4})",
|
||||
},
|
||||
{
|
||||
defaultToken: "string.quoted.single.env",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
token: "punctuation.definition.string.begin.env",
|
||||
regex: '"',
|
||||
push: [
|
||||
{
|
||||
token: "constant.language.escape",
|
||||
regex: "\\\\(?:[\\\\0abtrn;#=:]|x[a-fA-F\\d]{4})",
|
||||
},
|
||||
{
|
||||
token: "support.constant.color",
|
||||
regex: /\${[\w]+}/,
|
||||
},
|
||||
{
|
||||
token: "punctuation.definition.string.end.env",
|
||||
regex: '"',
|
||||
next: "pop",
|
||||
},
|
||||
{
|
||||
defaultToken: "string.quoted.double.env",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
token: "constant.language.boolean",
|
||||
regex: /(?:true|false)\b/,
|
||||
},
|
||||
],
|
||||
};
|
||||
this.normalizeRules();
|
||||
};
|
||||
|
||||
envHighlightRules.metaData = {
|
||||
fileTypes: ["env"],
|
||||
keyEquivalent: "^~I",
|
||||
name: "Env",
|
||||
scopeName: "source.env",
|
||||
};
|
||||
|
||||
oop.inherits(envHighlightRules, TextHighlightRules);
|
||||
|
||||
exports.envHighlightRules = envHighlightRules;
|
||||
});
|
||||
|
143
resources/js/ace-editor/mode-nginx.js
Normal file
143
resources/js/ace-editor/mode-nginx.js
Normal file
File diff suppressed because one or more lines are too long
47
resources/js/ace-editor/theme-vito.js
Normal file
47
resources/js/ace-editor/theme-vito.js
Normal file
@ -0,0 +1,47 @@
|
||||
ace.define(
|
||||
"ace/theme/vito",
|
||||
["require", "exports", "module", "ace/lib/dom"],
|
||||
function (require, exports) {
|
||||
(exports.isDark = true),
|
||||
(exports.cssClass = "ace-vito rounded-lg w-full"),
|
||||
(exports.cssText = `
|
||||
.ace-vito .ace_scrollbar::-webkit-scrollbar { width: 12px;}
|
||||
.ace-vito .ace_scrollbar::-webkit-scrollbar-track { background: #111827;}
|
||||
.ace-vito .ace_scrollbar::-webkit-scrollbar-thumb { background: #374151; border-radius: 4px;}
|
||||
.ace-vito .ace_gutter {background: #151c27;color: rgb(128,145,160)}
|
||||
.ace-vito .ace_print-margin {width: 1px;background: #555555}
|
||||
.ace-vito {background-color: #0f172a;color: #F9FAFB}
|
||||
.ace-vito .ace_cursor {color: #F9FAFB}
|
||||
.ace-vito .ace_marker-layer .ace_selection {background: rgba(179, 101, 57, 0.75)}
|
||||
.ace-vito.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #002240;}
|
||||
.ace-vito .ace_marker-layer .ace_step {background: rgb(127, 111, 19)}
|
||||
.ace-vito .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgba(255, 255, 255, 0.15)}
|
||||
.ace-vito .ace_marker-layer .ace_active-line {background: rgba(24, 182, 155, 0.10)}
|
||||
.ace-vito .ace_gutter-active-line {background-color: rgba(0, 0, 0, 0.35)}
|
||||
.ace-vito .ace_marker-layer .ace_selected-word {border: 1px solid rgba(179, 101, 57, 0.75)}
|
||||
.ace-vito .ace_invisible {color: rgba(255, 255, 255, 0.15)}
|
||||
.ace-vito .ace_keyword,.ace-vito .ace_meta {color: #FF9D00}
|
||||
.ace-vito .ace_constant,.ace-vito .ace_constant.ace_character,.ace-vito .ace_constant.ace_character.ace_escape,.ace-vito .ace_constant.ace_other {color: #FF628C}
|
||||
.ace-vito .ace_invalid {color: #F8F8F8;background-color: #800F00}
|
||||
.ace-vito .ace_support {color: #80FFBB}
|
||||
.ace-vito .ace_support.ace_constant {color: #EB939A}
|
||||
.ace-vito .ace_fold {background-color: #FF9D00;border-color: #F9FAFB}
|
||||
.ace-vito .ace_support.ace_function {color: #FFB054}
|
||||
.ace-vito .ace_storage {color: #FFEE80}
|
||||
.ace-vito .ace_entity {color: #FFDD00}
|
||||
.ace-vito .ace_string {color: #7cd827}
|
||||
.ace-vito .ace_string.ace_regexp {color: #80FFC2}
|
||||
.ace-vito .ace_comment {font-style: italic;color: #6B7280}
|
||||
.ace-vito .ace_heading,.ace-vito
|
||||
.ace_markup.ace_heading {color: #C8E4FD;background-color: #001221}
|
||||
.ace-vito .ace_list,.ace-vito .ace_markup.ace_list {background-color: #130D26}
|
||||
.ace-vito .ace_variable {color: #CCCCCC}
|
||||
.ace-vito .ace_variable.ace_language {color: #FF80E1}
|
||||
.ace-vito .ace_meta.ace_tag {color: #9EFFFF}
|
||||
.ace-vito .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYHCLSvkPAAP3AgSDTRd4AAAAAElFTkSuQmCC) right repeat-y}
|
||||
`);
|
||||
|
||||
var dom = require("../lib/dom");
|
||||
dom.importCssString(exports.cssText, exports.cssClass);
|
||||
},
|
||||
);
|
@ -1,5 +1,6 @@
|
||||
import 'flowbite';
|
||||
import 'flowbite/dist/datepicker.js';
|
||||
import './ace-editor/ace-editor';
|
||||
|
||||
import Alpine from 'alpinejs';
|
||||
window.Alpine = Alpine;
|
||||
|
@ -16,9 +16,8 @@ class="p-6"
|
||||
|
||||
<div class="mt-6">
|
||||
<x-input-label for="script" :value="__('Script')" />
|
||||
<x-textarea id="script" name="script" class="mt-1 min-h-[400px] w-full font-mono">
|
||||
{{ old("script", $site->deploymentScript?->content) }}
|
||||
</x-textarea>
|
||||
@php($value = old("script", $site->deploymentScript?->content))
|
||||
<x-editor id="script" name="script" lang="sh" :value="$value" />
|
||||
@error("script")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
|
@ -21,9 +21,8 @@ class="mt-6"
|
||||
>
|
||||
<x-input-label for="env" :value="__('.env')" />
|
||||
<div id="env-content">
|
||||
<x-textarea id="env" name="env" rows="10" class="mt-1 block min-h-[400px] w-full font-mono">
|
||||
{{ old("env", session()->get("env") ?? "Loading...") }}
|
||||
</x-textarea>
|
||||
@php($envValue = old("env", session()->get("env") ?? "Loading..."))
|
||||
<x-editor id="env" name="env" lang="env" :value="$envValue" />
|
||||
</div>
|
||||
@error("env")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
|
64
resources/views/components/autocomplete-text.blade.php
Normal file
64
resources/views/components/autocomplete-text.blade.php
Normal file
@ -0,0 +1,64 @@
|
||||
@props([
|
||||
"id",
|
||||
"name",
|
||||
"placeholder" => "Search...",
|
||||
"items" => [],
|
||||
"maxResults" => 5,
|
||||
"value" => "",
|
||||
])
|
||||
|
||||
<script>
|
||||
window['items_' + @js($id)] = @json($items);
|
||||
</script>
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
q: @js($value),
|
||||
items: window['items_' + @js($id)],
|
||||
resultItems: window['items_' + @js($id)],
|
||||
maxResults: @js($maxResults),
|
||||
init() {
|
||||
this.search()
|
||||
},
|
||||
search() {
|
||||
if (! this.q) {
|
||||
this.resultItems = this.items.slice(0, this.maxResults)
|
||||
return
|
||||
}
|
||||
this.resultItems = this.items
|
||||
.filter((item) => item.toLowerCase().includes(this.q.toLowerCase()))
|
||||
.slice(0, this.maxResults)
|
||||
},
|
||||
}"
|
||||
>
|
||||
<input type="hidden" name="{{ $name }}" x-ref="input" x-model="q" />
|
||||
<x-dropdown width="full" :hide-if-empty="true">
|
||||
<x-slot name="trigger">
|
||||
<x-text-input
|
||||
id="$id . '-q"
|
||||
x-model="q"
|
||||
type="text"
|
||||
class="mt-1 w-full"
|
||||
:placeholder="$placeholder"
|
||||
autocomplete="off"
|
||||
x-on:input.debounce.100ms="search"
|
||||
/>
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
<div
|
||||
id="{{ $id }}-items-list"
|
||||
x-bind:class="
|
||||
resultItems.length > 0
|
||||
? 'py-1 border border-gray-200 dark:border-gray-600 rounded-md'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<template x-for="item in resultItems">
|
||||
<x-dropdown-link class="cursor-pointer" x-on:click="q = item">
|
||||
<span x-text="item"></span>
|
||||
</x-dropdown-link>
|
||||
</template>
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
</div>
|
10
resources/views/components/dropdown-trigger.blade.php
Normal file
10
resources/views/components/dropdown-trigger.blade.php
Normal file
@ -0,0 +1,10 @@
|
||||
<div>
|
||||
<div
|
||||
class="block w-full cursor-pointer rounded-md border border-gray-300 p-2.5 text-sm focus:border-primary-500 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-primary-600 dark:focus:ring-primary-600"
|
||||
>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
<button type="button" class="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<x-heroicon name="o-chevron-down" class="h-4 w-4 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
@ -2,12 +2,18 @@
|
||||
"open" => false,
|
||||
"align" => "right",
|
||||
"width" => "48",
|
||||
"contentClasses" => "list-none divide-y divide-gray-100 rounded-md border border-gray-200 bg-white py-1 text-base dark:divide-gray-600 dark:border-gray-600 dark:bg-gray-700",
|
||||
"contentClasses" => "list-none divide-y divide-gray-100 rounded-md bg-white text-base dark:divide-gray-600 dark:bg-gray-700",
|
||||
"search" => false,
|
||||
"searchUrl" => "",
|
||||
"hideIfEmpty" => false,
|
||||
"closeOnClick" => true,
|
||||
])
|
||||
|
||||
@php
|
||||
if (! $hideIfEmpty) {
|
||||
$contentClasses .= " py-1 border border-gray-200 dark:border-gray-600";
|
||||
}
|
||||
|
||||
switch ($align) {
|
||||
case "left":
|
||||
$alignmentClasses = "left-0 origin-top-left";
|
||||
@ -25,6 +31,9 @@
|
||||
case "48":
|
||||
$width = "w-48";
|
||||
break;
|
||||
case "56":
|
||||
$width = "w-56";
|
||||
break;
|
||||
case "full":
|
||||
$width = "w-full";
|
||||
break;
|
||||
@ -46,31 +55,9 @@
|
||||
x-transition:leave-end="scale-95 transform opacity-0"
|
||||
class="{{ $width }} {{ $alignmentClasses }} absolute z-50 mt-2 rounded-md"
|
||||
style="display: none"
|
||||
@click="open = false"
|
||||
@if ($closeOnClick) @click="open = false" @endif
|
||||
>
|
||||
<div class="{{ $contentClasses }} rounded-md">
|
||||
@if ($search)
|
||||
<div class="p-2">
|
||||
<input
|
||||
type="text"
|
||||
x-ref="search"
|
||||
x-model="search"
|
||||
x-on:keydown.window.prevent.enter="open = false"
|
||||
x-on:keydown.window.prevent.escape="open = false"
|
||||
x-on:keydown.window.prevent.arrow-up="
|
||||
open = true
|
||||
$refs.search.focus()
|
||||
"
|
||||
x-on:keydown.window.prevent.arrow-down="
|
||||
open = true
|
||||
$refs.search.focus()
|
||||
"
|
||||
class="w-full rounded-md border border-gray-200 p-2"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="{{ $contentClasses }} max-h-80 overflow-y-auto rounded-md">
|
||||
{{ $content }}
|
||||
</div>
|
||||
</div>
|
||||
|
17
resources/views/components/editor.blade.php
Normal file
17
resources/views/components/editor.blade.php
Normal file
@ -0,0 +1,17 @@
|
||||
<div>
|
||||
<div
|
||||
id="{{ $id }}"
|
||||
{{ $attributes->merge(["class" => "mt-1 min-h-[400px] w-full"]) }}
|
||||
class="ace-vito ace_dark"
|
||||
></div>
|
||||
<textarea id="textarea-{{ $id }}" name="{{ $name }}" style="display: none"></textarea>
|
||||
<script>
|
||||
if (window.initAceEditor) {
|
||||
window.initAceEditor(@json($options));
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
window.initAceEditor(@json($options));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
14
resources/views/components/heroicons/o-funnel.blade.php
Normal file
14
resources/views/components/heroicons/o-funnel.blade.php
Normal file
@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
{{ $attributes }}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 0 1-.659 1.591l-5.432 5.432a2.25 2.25 0 0 0-.659 1.591v2.927a2.25 2.25 0 0 1-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 0 0-.659-1.591L3.659 7.409A2.25 2.25 0 0 1 3 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0 1 12 3Z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 556 B |
10
resources/views/components/heroicons/o-plus.blade.php
Normal file
10
resources/views/components/heroicons/o-plus.blade.php
Normal file
@ -0,0 +1,10 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
{{ $attributes }}
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
After Width: | Height: | Size: 251 B |
15
resources/views/components/heroicons/o-tag.blade.php
Normal file
15
resources/views/components/heroicons/o-tag.blade.php
Normal file
@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
{{ $attributes }}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z"
|
||||
/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 546 B |
@ -73,7 +73,7 @@ class="fixed inset-0 transform transition-all"
|
||||
|
||||
<div
|
||||
x-show="show"
|
||||
class="{{ $maxWidth }} mb-6 transform overflow-hidden rounded-lg bg-white shadow-xl transition-all dark:bg-gray-800 sm:mx-auto sm:w-full"
|
||||
class="{{ $maxWidth }} mb-6 transform overflow-visible rounded-lg bg-white shadow-xl transition-all dark:bg-gray-800 sm:mx-auto sm:w-full"
|
||||
x-transition:enter="duration-300 ease-out"
|
||||
x-transition:enter-start="translate-y-4 opacity-0 sm:translate-y-0 sm:scale-95"
|
||||
x-transition:enter-end="translate-y-0 opacity-100 sm:scale-100"
|
||||
|
@ -71,5 +71,6 @@
|
||||
</script>
|
||||
<x-toast />
|
||||
<x-htmx-error-handler />
|
||||
@stack("footer")
|
||||
</body>
|
||||
</html>
|
||||
|
@ -11,7 +11,12 @@
|
||||
</div>
|
||||
</header>
|
||||
@else
|
||||
<h2 class="text-lg font-semibold">{{ $server->name }}</h2>
|
||||
<div class="flex items-center">
|
||||
<h2 class="text-lg font-semibold">{{ $server->name }}</h2>
|
||||
<div class="ml-2">
|
||||
@include("settings.tags.tags", ["taggable" => $server])
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col items-end">
|
||||
|
@ -161,6 +161,13 @@ class="fixed left-0 top-0 z-40 h-screen w-64 -translate-x-full border-r border-g
|
||||
<x-hr />
|
||||
@endif
|
||||
|
||||
<li>
|
||||
<x-sidebar-link :href="route('servers')" :active="request()->routeIs('servers')">
|
||||
<x-heroicon name="o-server" class="h-6 w-6" />
|
||||
<span class="ml-2">Servers</span>
|
||||
</x-sidebar-link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<x-sidebar-link :href="route('scripts.index')" :active="request()->routeIs('scripts.*')">
|
||||
<x-heroicon name="o-bolt" class="h-6 w-6" />
|
||||
@ -239,6 +246,12 @@ class="fixed left-0 top-0 z-40 h-screen w-64 -translate-x-full border-r border-g
|
||||
<span class="ml-2">SSH Keys</span>
|
||||
</x-sidebar-link>
|
||||
</li>
|
||||
<li>
|
||||
<x-sidebar-link :href="route('settings.tags')" :active="request()->routeIs('settings.tags')">
|
||||
<x-heroicon name="o-tag" class="h-6 w-6" />
|
||||
<span class="ml-2">Tags</span>
|
||||
</x-sidebar-link>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -40,7 +40,7 @@ class="cursor-pointer"
|
||||
@foreach ([\App\Enums\PHPIniType::FPM, \App\Enums\PHPIniType::CLI] as $type)
|
||||
<x-dropdown-link
|
||||
class="cursor-pointer"
|
||||
x-on:click="version = '{{ $php->version }}'; $dispatch('open-modal', 'update-php-ini-{{ $type }}'); document.getElementById('ini').value = 'Loading...';"
|
||||
x-on:click="version = '{{ $php->version }}'; $dispatch('open-modal', 'update-php-ini-{{ $type }}');"
|
||||
hx-get="{{ route('servers.php.get-ini', ['server' => $server, 'version' => $php->version, 'type' => $type]) }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#update-php-ini-{{ $type }}-form"
|
||||
|
@ -15,9 +15,8 @@ class="p-6"
|
||||
|
||||
<div class="mt-6">
|
||||
<x-input-label for="ini" value="php.ini" />
|
||||
<x-textarea id="ini" name="ini" class="mt-1 w-full font-mono" rows="15">
|
||||
{{ old("ini", session()->get("ini")) }}
|
||||
</x-textarea>
|
||||
@php($ini = old("ini", session()->get("ini") ?? "Loading..."))
|
||||
<x-editor id="ini" name="ini" lang="ini" :value="$ini" />
|
||||
@error("ini")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
|
@ -1,7 +1,5 @@
|
||||
<x-input-label for="content" :value="__('Content')" />
|
||||
<x-textarea id="content" name="content" class="mt-1 min-h-[400px] w-full font-mono">
|
||||
{{ $value }}
|
||||
</x-textarea>
|
||||
<x-editor name="content" lang="sh" :value="$value" />
|
||||
@error("content")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
|
@ -110,4 +110,15 @@ class="mt-2 md:ml-2 md:mt-0"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="py-5">
|
||||
<div class="border-t border-gray-200 dark:border-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>{{ __("Tags") }}</div>
|
||||
<div>
|
||||
@include("settings.tags.tags", ["taggable" => $server, "edit" => true])
|
||||
</div>
|
||||
</div>
|
||||
</x-card>
|
||||
|
@ -9,40 +9,73 @@
|
||||
</x-slot>
|
||||
</x-card-header>
|
||||
|
||||
<x-live id="live-servers-list">
|
||||
@if (count($servers) > 0)
|
||||
<div class="space-y-3">
|
||||
@foreach ($servers as $server)
|
||||
<a href="{{ route("servers.show", ["server" => $server]) }}" class="block">
|
||||
<x-item-card>
|
||||
<div class="flex-none">
|
||||
<img
|
||||
src="{{ asset("static/images/" . $server->provider . ".svg") }}"
|
||||
class="h-10 w-10"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3 flex flex-grow flex-col items-start justify-center">
|
||||
<span class="mb-1">{{ $server->name }}</span>
|
||||
<span class="text-sm text-gray-400">
|
||||
{{ $server->ip }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="inline">
|
||||
@include("servers.partials.server-status", ["server" => $server])
|
||||
</div>
|
||||
</div>
|
||||
</x-item-card>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<x-simple-card>
|
||||
<div class="text-center">
|
||||
{{ __("You don't have any servers yet!") }}
|
||||
<div class="space-y-3">
|
||||
<x-live id="live-servers-list">
|
||||
@if (count($servers) > 0)
|
||||
<div class="space-y-3">
|
||||
<x-table>
|
||||
<x-thead>
|
||||
<x-tr>
|
||||
<x-th>Name</x-th>
|
||||
<x-th>IP</x-th>
|
||||
<x-th>Tags</x-th>
|
||||
<x-th>Status</x-th>
|
||||
<x-th></x-th>
|
||||
</x-tr>
|
||||
</x-thead>
|
||||
<x-tbody>
|
||||
@foreach ($servers as $server)
|
||||
<x-tr>
|
||||
<x-td>
|
||||
<div class="flex items-center">
|
||||
<img
|
||||
src="{{ asset("static/images/" . $server->provider . ".svg") }}"
|
||||
class="mr-1 h-5 w-5"
|
||||
alt=""
|
||||
/>
|
||||
<a
|
||||
href="{{ route("servers.show", ["server" => $server]) }}"
|
||||
class="hover:underline"
|
||||
>
|
||||
{{ $server->name }}
|
||||
</a>
|
||||
</div>
|
||||
</x-td>
|
||||
<x-td>{{ $server->ip }}</x-td>
|
||||
<x-td>
|
||||
@include("settings.tags.tags", ["taggable" => $server, "oobOff" => true])
|
||||
</x-td>
|
||||
<x-td>
|
||||
@include("servers.partials.server-status", ["server" => $server])
|
||||
</x-td>
|
||||
<x-td>
|
||||
<div class="flex items-center justify-end">
|
||||
<x-icon-button
|
||||
:href="route('servers.show', ['server' => $server])"
|
||||
data-tooltip="Show Server"
|
||||
>
|
||||
<x-heroicon name="o-eye" class="h-5 w-5" />
|
||||
</x-icon-button>
|
||||
<x-icon-button
|
||||
:href="route('servers.settings', ['server' => $server])"
|
||||
data-tooltip="Settings"
|
||||
>
|
||||
<x-heroicon name="o-wrench-screwdriver" class="h-5 w-5" />
|
||||
</x-icon-button>
|
||||
</div>
|
||||
</x-td>
|
||||
</x-tr>
|
||||
@endforeach
|
||||
</x-tbody>
|
||||
</x-table>
|
||||
</div>
|
||||
</x-simple-card>
|
||||
@endif
|
||||
</x-live>
|
||||
@else
|
||||
<x-simple-card>
|
||||
<div class="text-center">
|
||||
{{ __("You don't have any servers yet!") }}
|
||||
</div>
|
||||
</x-simple-card>
|
||||
@endif
|
||||
</x-live>
|
||||
</div>
|
||||
</x-container>
|
||||
|
@ -1,3 +1,14 @@
|
||||
@use(App\Enums\StorageProvider)
|
||||
@php
|
||||
$storageProviders = [
|
||||
StorageProvider::DROPBOX,
|
||||
StorageProvider::FTP,
|
||||
StorageProvider::LOCAL,
|
||||
StorageProvider::S3,
|
||||
StorageProvider::WASABI,
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div>
|
||||
<x-primary-button x-data="" x-on:click.prevent="$dispatch('open-modal', 'connect-provider')">
|
||||
{{ __("Connect") }}
|
||||
@ -69,136 +80,8 @@ class="p-6"
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@if ($provider == \App\Enums\StorageProvider::DROPBOX)
|
||||
<div class="mt-6">
|
||||
<x-input-label for="token" value="API Key" />
|
||||
<x-text-input value="{{ old('token') }}" id="token" name="token" type="text" class="mt-1 w-full" />
|
||||
@error("token")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
|
||||
<a
|
||||
class="mt-1 text-primary-500"
|
||||
href="https://dropbox.tech/developers/generate-an-access-token-for-your-own-account"
|
||||
target="_blank"
|
||||
>
|
||||
How to generate?
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($provider == \App\Enums\StorageProvider::FTP)
|
||||
<div class="mt-6">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="mt-6">
|
||||
<x-input-label for="host" value="Host" />
|
||||
<x-text-input
|
||||
value="{{ old('host') }}"
|
||||
id="host"
|
||||
name="host"
|
||||
type="text"
|
||||
class="mt-1 w-full"
|
||||
/>
|
||||
@error("host")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<x-input-label for="port" value="Port" />
|
||||
<x-text-input
|
||||
value="{{ old('port') }}"
|
||||
id="port"
|
||||
name="port"
|
||||
type="text"
|
||||
class="mt-1 w-full"
|
||||
/>
|
||||
@error("port")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<x-input-label for="path" value="Path" />
|
||||
<x-text-input value="{{ old('path') }}" id="path" name="path" type="text" class="mt-1 w-full" />
|
||||
@error("path")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="mt-6">
|
||||
<x-input-label for="username" value="Username" />
|
||||
<x-text-input
|
||||
value="{{ old('username') }}"
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
class="mt-1 w-full"
|
||||
/>
|
||||
@error("username")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<x-input-label for="password" value="Password" />
|
||||
<x-text-input
|
||||
value="{{ old('password') }}"
|
||||
id="password"
|
||||
name="password"
|
||||
type="text"
|
||||
class="mt-1 w-full"
|
||||
/>
|
||||
@error("password")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="mt-6">
|
||||
<x-input-label for="ssl" :value="__('SSL')" />
|
||||
<x-select-input id="ssl" name="ssl" class="mt-1 w-full">
|
||||
<option value="1" @if(old('ssl')) selected @endif>
|
||||
{{ __("Yes") }}
|
||||
</option>
|
||||
<option value="0" @if(!old('ssl')) selected @endif>
|
||||
{{ __("No") }}
|
||||
</option>
|
||||
</x-select-input>
|
||||
@error("ssl")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<x-input-label for="passive" :value="__('Passive')" />
|
||||
<x-select-input id="passive" name="passive" class="mt-1 w-full">
|
||||
<option value="1" @if(old('passive')) selected @endif>
|
||||
{{ __("Yes") }}
|
||||
</option>
|
||||
<option value="0" @if(!old('passive')) selected @endif>
|
||||
{{ __("No") }}
|
||||
</option>
|
||||
</x-select-input>
|
||||
@error("passive")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($provider == \App\Enums\StorageProvider::LOCAL)
|
||||
<div class="mt-6">
|
||||
<x-input-label for="path" value="Absolute Path" />
|
||||
<x-text-input value="{{ old('path') }}" id="path" name="path" type="text" class="mt-1 w-full" />
|
||||
<x-input-help>
|
||||
The absolute path on your server that the database exists. like `/home/vito/db-backups`
|
||||
</x-input-help>
|
||||
<x-input-help>
|
||||
Make sure that the path exists and the `vito` user has permission to write to it.
|
||||
</x-input-help>
|
||||
@error("path")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
@if (in_array($provider, $storageProviders))
|
||||
@include("settings.storage-providers.providers.{$provider}")
|
||||
@endif
|
||||
|
||||
<div class="mt-6">
|
||||
|
@ -0,0 +1,15 @@
|
||||
<div class="mt-6">
|
||||
<x-input-label for="token" value="API Key" />
|
||||
<x-text-input value="{{ old('token') }}" id="token" name="token" type="text" class="mt-1 w-full" />
|
||||
@error("token")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
|
||||
<a
|
||||
class="mt-1 text-primary-500"
|
||||
href="https://dropbox.tech/developers/generate-an-access-token-for-your-own-account"
|
||||
target="_blank"
|
||||
>
|
||||
How to generate?
|
||||
</a>
|
||||
</div>
|
@ -0,0 +1,71 @@
|
||||
<div class="mt-6">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="mt-6">
|
||||
<x-input-label for="host" value="Host" />
|
||||
<x-text-input value="{{ old('host') }}" id="host" name="host" type="text" class="mt-1 w-full" />
|
||||
@error("host")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<x-input-label for="port" value="Port" />
|
||||
<x-text-input value="{{ old('port') }}" id="port" name="port" type="text" class="mt-1 w-full" />
|
||||
@error("port")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<x-input-label for="path" value="Path" />
|
||||
<x-text-input value="{{ old('path') }}" id="path" name="path" type="text" class="mt-1 w-full" />
|
||||
@error("path")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="mt-6">
|
||||
<x-input-label for="username" value="Username" />
|
||||
<x-text-input value="{{ old('username') }}" id="username" name="username" type="text" class="mt-1 w-full" />
|
||||
@error("username")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<x-input-label for="password" value="Password" />
|
||||
<x-text-input value="{{ old('password') }}" id="password" name="password" type="text" class="mt-1 w-full" />
|
||||
@error("password")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="mt-6">
|
||||
<x-input-label for="ssl" :value="__('SSL')" />
|
||||
<x-select-input id="ssl" name="ssl" class="mt-1 w-full">
|
||||
<option value="1" @if(old('ssl')) selected @endif>
|
||||
{{ __("Yes") }}
|
||||
</option>
|
||||
<option value="0" @if(!old('ssl')) selected @endif>
|
||||
{{ __("No") }}
|
||||
</option>
|
||||
</x-select-input>
|
||||
@error("ssl")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<x-input-label for="passive" :value="__('Passive')" />
|
||||
<x-select-input id="passive" name="passive" class="mt-1 w-full">
|
||||
<option value="1" @if(old('passive')) selected @endif>
|
||||
{{ __("Yes") }}
|
||||
</option>
|
||||
<option value="0" @if(!old('passive')) selected @endif>
|
||||
{{ __("No") }}
|
||||
</option>
|
||||
</x-select-input>
|
||||
@error("passive")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,9 @@
|
||||
<div class="mt-6">
|
||||
<x-input-label for="path" value="Absolute Path" />
|
||||
<x-text-input value="{{ old('path') }}" id="path" name="path" type="text" class="mt-1 w-full" />
|
||||
<x-input-help>The absolute path on your server that the database exists. like `/home/vito/db-backups`</x-input-help>
|
||||
<x-input-help>Make sure that the path exists and the `vito` user has permission to write to it.</x-input-help>
|
||||
@error("path")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
@ -0,0 +1,49 @@
|
||||
<div class="mt-6">
|
||||
<div class="mt-6">
|
||||
<x-input-label for="path" value="Path" />
|
||||
<x-text-input value="{{ old('path') }}" id="path" name="path" type="text" class="mt-1 w-full" />
|
||||
@error("path")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="mt-6">
|
||||
<x-input-label for="key" value="Key" />
|
||||
<x-text-input value="{{ old('key') }}" id="key" name="key" type="text" class="mt-1 w-full" />
|
||||
@error("key")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<x-input-label for="secret" value="Secret" />
|
||||
<x-text-input value="{{ old('secret') }}" id="secret" name="secret" type="text" class="mt-1 w-full" />
|
||||
@error("secret")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<a
|
||||
class="mt-1 text-primary-500"
|
||||
href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/configuring-bucket-key.html"
|
||||
target="_blank"
|
||||
>
|
||||
How to generate?
|
||||
</a>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="mt-6">
|
||||
<x-input-label for="region" value="Region" />
|
||||
<x-text-input value="{{ old('region') }}" id="region" name="region" type="text" class="mt-1 w-full" />
|
||||
@error("region")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<x-input-label for="bucket" value="Bucket" />
|
||||
<x-text-input value="{{ old('bucket') }}" id="bucket" name="bucket" type="text" class="mt-1 w-full" />
|
||||
@error("bucket")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,49 @@
|
||||
<div class="mt-6">
|
||||
<div class="mt-6">
|
||||
<x-input-label for="path" value="Path" />
|
||||
<x-text-input value="{{ old('path') }}" id="path" name="path" type="text" class="mt-1 w-full" />
|
||||
@error("path")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="mt-6">
|
||||
<x-input-label for="key" value="Key" />
|
||||
<x-text-input value="{{ old('key') }}" id="key" name="key" type="text" class="mt-1 w-full" />
|
||||
@error("key")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<x-input-label for="secret" value="Secret" />
|
||||
<x-text-input value="{{ old('secret') }}" id="secret" name="secret" type="text" class="mt-1 w-full" />
|
||||
@error("secret")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<a
|
||||
class="mt-1 text-primary-500"
|
||||
href="https://docs.wasabi.com/docs/creating-a-user-account-and-access-key"
|
||||
target="_blank"
|
||||
>
|
||||
How to generate?
|
||||
</a>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="mt-6">
|
||||
<x-input-label for="region" value="Region" />
|
||||
<x-text-input value="{{ old('region') }}" id="region" name="region" type="text" class="mt-1 w-full" />
|
||||
@error("region")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<x-input-label for="bucket" value="Bucket" />
|
||||
<x-text-input value="{{ old('bucket') }}" id="bucket" name="bucket" type="text" class="mt-1 w-full" />
|
||||
@error("bucket")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
41
resources/views/settings/tags/attach.blade.php
Normal file
41
resources/views/settings/tags/attach.blade.php
Normal file
@ -0,0 +1,41 @@
|
||||
<div x-data="">
|
||||
<button type="button" class="flex items-center" x-on:click="$dispatch('open-modal', 'create-tag-modal')">
|
||||
<x-heroicon name="o-plus" class="h-5 w-5 text-gray-500 dark:text-gray-400" />
|
||||
<span class="text-md">New Tag</span>
|
||||
</button>
|
||||
@push("footer")
|
||||
<x-modal name="create-tag-modal" max-width="sm">
|
||||
<form
|
||||
id="create-tag-form"
|
||||
hx-post="{{ route("tags.attach") }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-select="#create-tag-form"
|
||||
class="p-6"
|
||||
>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">New Tag</h2>
|
||||
|
||||
<input type="hidden" name="taggable_type" value="{{ get_class($taggable) }}" />
|
||||
<input type="hidden" name="taggable_id" value="{{ $taggable->id }}" />
|
||||
|
||||
<div class="mt-6">
|
||||
@include("settings.tags.fields.name",["value" => old("name"),"items" => auth()->user()->currentProject->tags()->pluck("name"),])
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<x-secondary-button type="button" x-on:click="$dispatch('close')">Cancel</x-secondary-button>
|
||||
<x-primary-button class="ml-3" hx-disable>Save</x-primary-button>
|
||||
</div>
|
||||
|
||||
@if (session()->has("status") && session()->get("status") === "tag-created")
|
||||
<script defer>
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('close-modal', {
|
||||
detail: 'create-tag-modal'
|
||||
})
|
||||
);
|
||||
</script>
|
||||
@endif
|
||||
</form>
|
||||
</x-modal>
|
||||
@endpush
|
||||
</div>
|
31
resources/views/settings/tags/fields/color.blade.php
Normal file
31
resources/views/settings/tags/fields/color.blade.php
Normal file
@ -0,0 +1,31 @@
|
||||
@php($id = "color-" . uniqid())
|
||||
<div x-data="{
|
||||
value: '{{ $value }}',
|
||||
}">
|
||||
<x-input-label for="color" :value="__('Color')" />
|
||||
<input x-bind:value="value" id="{{ $id }}" name="color" type="hidden" />
|
||||
<x-dropdown class="relative" align="left">
|
||||
<x-slot name="trigger">
|
||||
<x-dropdown-trigger>
|
||||
<div class="flex items-center">
|
||||
<div x-show="value" x-bind:class="`bg-${value}-500 mr-1 h-3 w-3 rounded-full`"></div>
|
||||
<span x-text="value || 'Select a color'"></span>
|
||||
</div>
|
||||
</x-dropdown-trigger>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="z-50 max-h-[200px] overflow-y-auto">
|
||||
@foreach (config("core.tag_colors") as $color)
|
||||
<x-dropdown-link href="#" x-on:click="value = '{{ $color }}'" class="flex items-center capitalize">
|
||||
<div class="bg-{{ $color }}-500 mr-1 h-3 w-3 rounded-full"></div>
|
||||
{{ $color }}
|
||||
</x-dropdown-link>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
@error("color")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
||||
</div>
|
12
resources/views/settings/tags/fields/name.blade.php
Normal file
12
resources/views/settings/tags/fields/name.blade.php
Normal file
@ -0,0 +1,12 @@
|
||||
@php
|
||||
$id = "name-" . uniqid();
|
||||
if (! isset($items)) {
|
||||
$items = [];
|
||||
}
|
||||
@endphp
|
||||
|
||||
<x-input-label :for="$id" :value="__('Name')" />
|
||||
<x-autocomplete-text id="tag-name" name="name" :items="$items" :value="$value" placeholder="" />
|
||||
@error("name")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
@enderror
|
5
resources/views/settings/tags/index.blade.php
Normal file
5
resources/views/settings/tags/index.blade.php
Normal file
@ -0,0 +1,5 @@
|
||||
<x-settings-layout>
|
||||
<x-slot name="pageTitle">{{ __("Tags") }}</x-slot>
|
||||
|
||||
@include("settings.tags.partials.tags-list")
|
||||
</x-settings-layout>
|
42
resources/views/settings/tags/manage.blade.php
Normal file
42
resources/views/settings/tags/manage.blade.php
Normal file
@ -0,0 +1,42 @@
|
||||
<x-modal name="manage-tags-modal">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">Manage Tags</h2>
|
||||
@include("settings.tags.attach", ["taggable" => $taggable])
|
||||
</div>
|
||||
|
||||
<x-table id="tags-{{ $taggable->id }}" class="mt-6" hx-swap-oob="outerHTML">
|
||||
<x-thead>
|
||||
<x-tr>
|
||||
<x-th>Name</x-th>
|
||||
<x-th></x-th>
|
||||
</x-tr>
|
||||
</x-thead>
|
||||
<x-tbody>
|
||||
@foreach ($taggable->tags as $tag)
|
||||
<x-tr>
|
||||
<x-td>
|
||||
<div class="flex items-center">
|
||||
<div class="bg-{{ $tag->color }}-500 mr-1 h-3 w-3 rounded-full"></div>
|
||||
{{ $tag->name }}
|
||||
</div>
|
||||
</x-td>
|
||||
<x-td class="text-right">
|
||||
<form
|
||||
id="detach-tag-{{ $tag->id }}"
|
||||
hx-post="{{ route("tags.detach", ["tag" => $tag]) }}"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="taggable_type" value="{{ get_class($taggable) }}" />
|
||||
<input type="hidden" name="taggable_id" value="{{ $taggable->id }}" />
|
||||
<x-icon-button>
|
||||
<x-heroicon name="o-trash" class="h-5 w-5 text-gray-500 dark:text-gray-400" />
|
||||
</x-icon-button>
|
||||
</form>
|
||||
</x-td>
|
||||
</x-tr>
|
||||
@endforeach
|
||||
</x-tbody>
|
||||
</x-table>
|
||||
</div>
|
||||
</x-modal>
|
41
resources/views/settings/tags/partials/create-tag.blade.php
Normal file
41
resources/views/settings/tags/partials/create-tag.blade.php
Normal file
@ -0,0 +1,41 @@
|
||||
<div>
|
||||
<x-primary-button x-data="" x-on:click.prevent="$dispatch('open-modal', 'create-tag')">
|
||||
{{ __("Create Tag") }}
|
||||
</x-primary-button>
|
||||
|
||||
<x-modal name="create-tag">
|
||||
<form
|
||||
id="create-tag-form"
|
||||
hx-post="{{ route("settings.tags.create") }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-select="#create-tag-form"
|
||||
hx-ext="disable-element"
|
||||
hx-disable-element="#btn-create-tag"
|
||||
class="p-6"
|
||||
x-data="{}"
|
||||
>
|
||||
@csrf
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __("Create Tag") }}
|
||||
</h2>
|
||||
|
||||
<div class="mt-6">
|
||||
@include("settings.tags.fields.name", ["value" => old("name")])
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
@include("settings.tags.fields.color", ["value" => old("color")])
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<x-secondary-button type="button" x-on:click="$dispatch('close')">
|
||||
{{ __("Cancel") }}
|
||||
</x-secondary-button>
|
||||
|
||||
<x-primary-button id="btn-create-tag" class="ml-3">
|
||||
{{ __("Save") }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-modal>
|
||||
</div>
|
18
resources/views/settings/tags/partials/delete-tag.blade.php
Normal file
18
resources/views/settings/tags/partials/delete-tag.blade.php
Normal file
@ -0,0 +1,18 @@
|
||||
<x-modal name="delete-tag" :show="$errors->isNotEmpty()">
|
||||
<form id="delete-tag-form" method="post" x-bind:action="deleteAction" class="p-6">
|
||||
@csrf
|
||||
@method("delete")
|
||||
|
||||
<h2 class="text-lg font-medium">Deleting a tag will detach it from all the resources that it has been used</h2>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<x-secondary-button type="button" x-on:click="$dispatch('close')">
|
||||
{{ __("Cancel") }}
|
||||
</x-secondary-button>
|
||||
|
||||
<x-danger-button class="ml-3">
|
||||
{{ __("Delete") }}
|
||||
</x-danger-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-modal>
|
39
resources/views/settings/tags/partials/edit-tag.blade.php
Normal file
39
resources/views/settings/tags/partials/edit-tag.blade.php
Normal file
@ -0,0 +1,39 @@
|
||||
<x-modal
|
||||
name="edit-tag"
|
||||
:show="true"
|
||||
x-on:modal-edit-tag-closed.window="window.history.pushState('', '', '{{ route('settings.tags') }}');"
|
||||
>
|
||||
<form
|
||||
id="edit-tag-form"
|
||||
hx-post="{{ route("settings.tags.update", ["tag" => $tag->id]) }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-select="#edit-tag-form"
|
||||
hx-ext="disable-element"
|
||||
hx-disable-element="#btn-edit-tag"
|
||||
class="p-6"
|
||||
>
|
||||
@csrf
|
||||
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __("Edit Tag") }}
|
||||
</h2>
|
||||
|
||||
<div class="mt-6">
|
||||
@include("settings.tags.fields.name", ["value" => old("name", $tag->name)])
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
@include("settings.tags.fields.color", ["value" => old("color", $tag->color)])
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<x-secondary-button type="button" x-on:click="$dispatch('close')">
|
||||
{{ __("Cancel") }}
|
||||
</x-secondary-button>
|
||||
|
||||
<x-primary-button id="btn-edit-tag" class="ml-3">
|
||||
{{ __("Save") }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-modal>
|
69
resources/views/settings/tags/partials/tags-list.blade.php
Normal file
69
resources/views/settings/tags/partials/tags-list.blade.php
Normal file
@ -0,0 +1,69 @@
|
||||
<div>
|
||||
<x-card-header>
|
||||
<x-slot name="title">{{ __("Tags") }}</x-slot>
|
||||
<x-slot name="description">
|
||||
{{ __("You can manage tags here") }}
|
||||
</x-slot>
|
||||
<x-slot name="aside">
|
||||
@include("settings.tags.partials.create-tag")
|
||||
</x-slot>
|
||||
</x-card-header>
|
||||
<div x-data="{ deleteAction: '' }" class="space-y-3">
|
||||
@if (count($tags) > 0)
|
||||
<x-table class="mt-6">
|
||||
<x-thead>
|
||||
<x-tr>
|
||||
<x-th>Name</x-th>
|
||||
<x-th></x-th>
|
||||
</x-tr>
|
||||
</x-thead>
|
||||
<x-tbody>
|
||||
@foreach ($tags as $tag)
|
||||
<x-tr>
|
||||
<x-td>
|
||||
<div class="flex items-center">
|
||||
<div class="bg-{{ $tag->color }}-500 mr-1 size-4 rounded-full"></div>
|
||||
{{ $tag->name }}
|
||||
</div>
|
||||
</x-td>
|
||||
<x-td class="text-right">
|
||||
<div class="inline">
|
||||
<x-icon-button
|
||||
id="edit-{{ $tag->id }}"
|
||||
hx-get="{{ route('settings.tags', ['edit' => $tag->id]) }}"
|
||||
hx-replace-url="true"
|
||||
hx-select="#edit"
|
||||
hx-target="#edit"
|
||||
hx-ext="disable-element"
|
||||
hx-disable-element="#edit-{{ $tag->id }}"
|
||||
>
|
||||
<x-heroicon name="o-pencil" class="h-5 w-5" />
|
||||
</x-icon-button>
|
||||
<x-icon-button
|
||||
x-on:click="deleteAction = '{{ route('settings.tags.delete', ['tag' => $tag]) }}'; $dispatch('open-modal', 'delete-tag')"
|
||||
>
|
||||
<x-heroicon name="o-trash" class="h-5 w-5" />
|
||||
</x-icon-button>
|
||||
</div>
|
||||
</x-td>
|
||||
</x-tr>
|
||||
@endforeach
|
||||
</x-tbody>
|
||||
</x-table>
|
||||
|
||||
@include("settings.tags.partials.delete-tag")
|
||||
|
||||
<div id="edit">
|
||||
@if (isset($editTag))
|
||||
@include("settings.tags.partials.edit-tag", ["tag" => $editTag])
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<x-simple-card>
|
||||
<div class="text-center">
|
||||
{{ __("You don't have any tags yet!") }}
|
||||
</div>
|
||||
</x-simple-card>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
30
resources/views/settings/tags/tags.blade.php
Normal file
30
resources/views/settings/tags/tags.blade.php
Normal file
@ -0,0 +1,30 @@
|
||||
<div x-data="">
|
||||
<div class="inline-flex gap-1">
|
||||
<div
|
||||
id="tags-list-{{ $taggable->id }}"
|
||||
class="inline-flex gap-1"
|
||||
@if (! isset($oobOff) || ! $oobOff)
|
||||
hx-swap-oob="outerHTML"
|
||||
@endif
|
||||
>
|
||||
@foreach ($taggable->tags as $tag)
|
||||
<div
|
||||
class="border-{{ $tag->color }}-300 bg-{{ $tag->color }}-50 text-{{ $tag->color }}-500 dark:border-{{ $tag->color }}-600 dark:bg-{{ $tag->color }}-500 flex max-w-max items-center rounded-md border px-2 py-1 text-xs dark:bg-opacity-10"
|
||||
>
|
||||
<x-heroicon name="o-tag" class="mr-1 size-4" />
|
||||
{{ $tag->name }}
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if (isset($edit) && $edit)
|
||||
<a
|
||||
x-on:click="$dispatch('open-modal', 'manage-tags-modal')"
|
||||
class="flex max-w-max cursor-pointer items-center justify-center rounded-md border border-gray-300 bg-gray-50 px-2 py-1 text-xs text-gray-500 dark:border-gray-600 dark:bg-gray-500 dark:bg-opacity-10"
|
||||
>
|
||||
<x-heroicon name="o-pencil" class="h-3 w-3" />
|
||||
</a>
|
||||
@include("settings.tags.manage")
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
@ -1,6 +1,8 @@
|
||||
<x-site-layout :site="$site">
|
||||
<x-slot name="pageTitle">{{ __("Settings") }}</x-slot>
|
||||
|
||||
@include("site-settings.partials.site-details")
|
||||
|
||||
@include("site-settings.partials.change-php-version")
|
||||
|
||||
@include("site-settings.partials.update-aliases")
|
||||
|
@ -0,0 +1,56 @@
|
||||
<x-card id="server-details">
|
||||
<x-slot name="title">{{ __("Details") }}</x-slot>
|
||||
<x-slot name="description">
|
||||
{{ __("More details about your site") }}
|
||||
</x-slot>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>{{ __("Created At") }}</div>
|
||||
<div>
|
||||
<x-datetime :value="$site->created_at" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="py-5">
|
||||
<div class="border-t border-gray-200 dark:border-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>{{ __("Type") }}</div>
|
||||
<div class="capitalize">{{ $site->type }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="py-5">
|
||||
<div class="border-t border-gray-200 dark:border-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>{{ __("Site ID") }}</div>
|
||||
<div class="flex items-center">
|
||||
<span class="rounded-md bg-gray-100 p-1 dark:bg-gray-700">
|
||||
{{ $site->id }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="py-5">
|
||||
<div class="border-t border-gray-200 dark:border-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>{{ __("Status") }}</div>
|
||||
<div class="flex items-center">
|
||||
@include("sites.partials.site-status")
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="py-5">
|
||||
<div class="border-t border-gray-200 dark:border-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>{{ __("Tags") }}</div>
|
||||
<div>
|
||||
@include("settings.tags.tags", ["taggable" => $site, "edit" => true])
|
||||
</div>
|
||||
</div>
|
||||
</x-card>
|
@ -22,9 +22,8 @@ class="space-y-6"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<div id="vhost-container">
|
||||
<x-textarea id="vhost" name="vhost" rows="10" class="mt-1 block min-h-[400px] w-full font-mono">
|
||||
{{ session()->has("vhost") ? session()->get("vhost") : "Loading..." }}
|
||||
</x-textarea>
|
||||
@php($vhost = old("vhost", session()->get("vhost") ?? "Loading..."))
|
||||
<x-editor id="vhost" name="vhost" lang="nginx" :value="$vhost" />
|
||||
</div>
|
||||
@error("vhost")
|
||||
<x-input-error class="mt-2" :messages="$message" />
|
||||
|
@ -12,30 +12,63 @@
|
||||
<x-live id="live-sites">
|
||||
@if (count($sites) > 0)
|
||||
<div class="space-y-3">
|
||||
@foreach ($sites as $site)
|
||||
<a href="{{ route("servers.sites.show", ["server" => $server, "site" => $site]) }}" class="block">
|
||||
<x-item-card>
|
||||
<div class="flex-none">
|
||||
<img
|
||||
src="{{ asset("static/images/" . $site->type . ".svg") }}"
|
||||
class="h-10 w-10"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3 flex flex-grow flex-col items-start justify-center">
|
||||
<span class="mb-1">{{ $site->domain }}</span>
|
||||
<span class="text-sm text-gray-400">
|
||||
<x-table>
|
||||
<x-thead>
|
||||
<x-tr>
|
||||
<x-th>Domain</x-th>
|
||||
<x-th>Date</x-th>
|
||||
<x-th>Tags</x-th>
|
||||
<x-th>Status</x-th>
|
||||
<x-th></x-th>
|
||||
</x-tr>
|
||||
</x-thead>
|
||||
<x-tbody>
|
||||
@foreach ($sites as $site)
|
||||
<x-tr>
|
||||
<x-td>
|
||||
<div class="flex items-center">
|
||||
<img
|
||||
src="{{ asset("static/images/" . $site->type . ".svg") }}"
|
||||
class="mr-1 h-5 w-5"
|
||||
alt=""
|
||||
/>
|
||||
<a
|
||||
href="{{ route("servers.sites.show", ["server" => $server, "site" => $site]) }}"
|
||||
class="hover:underline"
|
||||
>
|
||||
{{ $site->domain }}
|
||||
</a>
|
||||
</div>
|
||||
</x-td>
|
||||
<x-td>
|
||||
<x-datetime :value="$site->created_at" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="inline">
|
||||
</x-td>
|
||||
<x-td>
|
||||
@include("settings.tags.tags", ["taggable" => $site, "oobOff" => true])
|
||||
</x-td>
|
||||
<x-td>
|
||||
@include("sites.partials.status", ["status" => $site->status])
|
||||
</div>
|
||||
</div>
|
||||
</x-item-card>
|
||||
</a>
|
||||
@endforeach
|
||||
</x-td>
|
||||
<x-td>
|
||||
<div class="flex items-center justify-end">
|
||||
<x-icon-button
|
||||
:href="route('servers.sites.show', ['server' => $server, 'site' => $site])"
|
||||
data-tooltip="Show Site"
|
||||
>
|
||||
<x-heroicon name="o-eye" class="h-5 w-5" />
|
||||
</x-icon-button>
|
||||
<x-icon-button
|
||||
:href="route('servers.sites.settings', ['server' => $server, 'site' => $site])"
|
||||
data-tooltip="Settings"
|
||||
>
|
||||
<x-heroicon name="o-wrench-screwdriver" class="h-5 w-5" />
|
||||
</x-icon-button>
|
||||
</div>
|
||||
</x-td>
|
||||
</x-tr>
|
||||
@endforeach
|
||||
</x-tbody>
|
||||
</x-table>
|
||||
</div>
|
||||
@else
|
||||
<x-simple-card>
|
||||
|
@ -6,6 +6,7 @@
|
||||
use App\Http\Controllers\Settings\SourceControlController;
|
||||
use App\Http\Controllers\Settings\SSHKeyController;
|
||||
use App\Http\Controllers\Settings\StorageProviderController;
|
||||
use App\Http\Controllers\Settings\TagController;
|
||||
use App\Http\Controllers\Settings\UserController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
@ -64,3 +65,11 @@
|
||||
Route::post('add', [SshKeyController::class, 'add'])->name('settings.ssh-keys.add');
|
||||
Route::delete('delete/{id}', [SshKeyController::class, 'delete'])->name('settings.ssh-keys.delete');
|
||||
});
|
||||
|
||||
// tags
|
||||
Route::prefix('/tags')->group(function () {
|
||||
Route::get('/', [TagController::class, 'index'])->name('settings.tags');
|
||||
Route::post('/create', [TagController::class, 'create'])->name('settings.tags.create');
|
||||
Route::post('/{tag}', [TagController::class, 'update'])->name('settings.tags.update');
|
||||
Route::delete('/{tag}', [TagController::class, 'delete'])->name('settings.tags.delete');
|
||||
});
|
||||
|
@ -4,6 +4,7 @@
|
||||
use App\Http\Controllers\ScriptController;
|
||||
use App\Http\Controllers\SearchController;
|
||||
use App\Http\Controllers\Settings\ProjectController;
|
||||
use App\Http\Controllers\Settings\TagController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
@ -25,6 +26,11 @@
|
||||
require __DIR__.'/settings.php';
|
||||
});
|
||||
|
||||
Route::prefix('/settings/tags')->group(function () {
|
||||
Route::post('/attach', [TagController::class, 'attach'])->name('tags.attach');
|
||||
Route::post('/{tag}/detach', [TagController::class, 'detach'])->name('tags.detach');
|
||||
});
|
||||
|
||||
Route::prefix('/servers')->middleware('must-have-current-project')->group(function () {
|
||||
require __DIR__.'/server.php';
|
||||
});
|
||||
|
@ -24,7 +24,7 @@ fi
|
||||
|
||||
if [[ -z "${V_ADMIN_PASSWORD}" ]]; then
|
||||
echo "Enter a password for Vito's dashboard:"
|
||||
read -s V_ADMIN_PASSWORD
|
||||
read V_ADMIN_PASSWORD
|
||||
fi
|
||||
|
||||
if [[ -z "${V_ADMIN_PASSWORD}" ]]; then
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user