mirror of
https://github.com/vitodeploy/vito.git
synced 2025-04-19 09:51:37 +00:00
* feat: Implement Site Commands (#298) - Introduced a Commands widget/table for Site view, allowing users to create, edit, delete, and execute commands. - Each Site Type now has a predefined set of commands inserted upon site creation. - A migration script ensures commands are created for existing sites. - Implemented necessary policies for command management. - Added feature tests to validate functionality. * I'm trying to fix the tests, but it seems like it might not work. I'm having trouble running the tests locally for some reason. * I'm trying to fix the tests, but it seems like it might not work. I'm having trouble running the tests locally for some reason. * I'm trying to fix the tests, but it seems like it might not work. I'm having trouble running the tests locally for some reason. * I'm trying to fix the tests, but it seems like it might not work. I'm having trouble running the tests locally for some reason. * Remove feature tests for commands due to inconsistencies for now * fixes * add tests for commands * ui fix and add to wordpress --------- Co-authored-by: Saeed Vaziry <mr.saeedvaziry@gmail.com>
This commit is contained in:
parent
176ff3bbc4
commit
97e20206e8
29
app/Actions/Site/CreateCommand.php
Normal file
29
app/Actions/Site/CreateCommand.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Site;
|
||||
|
||||
use App\Models\Command;
|
||||
use App\Models\Site;
|
||||
|
||||
class CreateCommand
|
||||
{
|
||||
public function create(Site $site, array $input): Command
|
||||
{
|
||||
$script = new Command([
|
||||
'site_id' => $site->id,
|
||||
'name' => $input['name'],
|
||||
'command' => $input['command'],
|
||||
]);
|
||||
$script->save();
|
||||
|
||||
return $script;
|
||||
}
|
||||
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'command' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
@ -68,6 +68,9 @@ public function create(Server $server, array $input): Site
|
||||
'content' => '',
|
||||
]);
|
||||
|
||||
// create base commands if any
|
||||
$site->commands()->createMany($site->type()->baseCommands());
|
||||
|
||||
// install site
|
||||
dispatch(function () use ($site) {
|
||||
$site->type()->install();
|
||||
|
25
app/Actions/Site/EditCommand.php
Normal file
25
app/Actions/Site/EditCommand.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Site;
|
||||
|
||||
use App\Models\Command;
|
||||
|
||||
class EditCommand
|
||||
{
|
||||
public function edit(Command $command, array $input): Command
|
||||
{
|
||||
$command->name = $input['name'];
|
||||
$command->command = $input['command'];
|
||||
$command->save();
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'command' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
58
app/Actions/Site/ExecuteCommand.php
Normal file
58
app/Actions/Site/ExecuteCommand.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Site;
|
||||
|
||||
use App\Enums\CommandExecutionStatus;
|
||||
use App\Models\Command;
|
||||
use App\Models\CommandExecution;
|
||||
use App\Models\ServerLog;
|
||||
use App\Models\User;
|
||||
|
||||
class ExecuteCommand
|
||||
{
|
||||
public function execute(Command $command, User $user, array $input): CommandExecution
|
||||
{
|
||||
$execution = new CommandExecution([
|
||||
'command_id' => $command->id,
|
||||
'server_id' => $command->site->server_id,
|
||||
'user_id' => $user->id,
|
||||
'variables' => $input['variables'] ?? [],
|
||||
'status' => CommandExecutionStatus::EXECUTING,
|
||||
]);
|
||||
$execution->save();
|
||||
|
||||
dispatch(function () use ($execution, $command) {
|
||||
$content = $execution->getContent();
|
||||
$log = ServerLog::make($execution->server, 'command-'.$command->id.'-'.strtotime('now'));
|
||||
$log->save();
|
||||
$execution->server_log_id = $log->id;
|
||||
$execution->save();
|
||||
$execution->server->os()->runScript(
|
||||
path: $command->site->path,
|
||||
script: $content,
|
||||
user: $command->site->user,
|
||||
serverLog: $log,
|
||||
variables: $execution->variables
|
||||
);
|
||||
$execution->status = CommandExecutionStatus::COMPLETED;
|
||||
$execution->save();
|
||||
})->catch(function () use ($execution) {
|
||||
$execution->status = CommandExecutionStatus::FAILED;
|
||||
$execution->save();
|
||||
})->onConnection('ssh');
|
||||
|
||||
return $execution;
|
||||
}
|
||||
|
||||
public static function rules(array $input): array
|
||||
{
|
||||
return [
|
||||
'variables' => 'array',
|
||||
'variables.*' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
12
app/Enums/CommandExecutionStatus.php
Normal file
12
app/Enums/CommandExecutionStatus.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
final class CommandExecutionStatus
|
||||
{
|
||||
const EXECUTING = 'executing';
|
||||
|
||||
const COMPLETED = 'completed';
|
||||
|
||||
const FAILED = 'failed';
|
||||
}
|
@ -11,4 +11,6 @@ final class SiteFeature
|
||||
const SSL = 'ssl';
|
||||
|
||||
const QUEUES = 'queues';
|
||||
|
||||
const COMMANDS = 'commands';
|
||||
}
|
||||
|
71
app/Models/Command.php
Normal file
71
app/Models/Command.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $site_id
|
||||
* @property string $name
|
||||
* @property string $command
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property Collection<CommandExecution> $executions
|
||||
* @property ?CommandExecution $lastExecution
|
||||
* @property Site $site
|
||||
*/
|
||||
class Command extends AbstractModel
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'site_id',
|
||||
'name',
|
||||
'command',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'site_id' => 'integer',
|
||||
];
|
||||
|
||||
public static function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::deleting(function (Command $command) {
|
||||
$command->executions()->delete();
|
||||
});
|
||||
}
|
||||
|
||||
public function site(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Site::class);
|
||||
}
|
||||
|
||||
public function getVariables(): array
|
||||
{
|
||||
$variables = [];
|
||||
preg_match_all('/\${(.*?)}/', $this->command, $matches);
|
||||
foreach ($matches[1] as $match) {
|
||||
$variables[] = $match;
|
||||
}
|
||||
|
||||
return array_unique($variables);
|
||||
}
|
||||
|
||||
public function executions(): HasMany
|
||||
{
|
||||
return $this->hasMany(CommandExecution::class);
|
||||
}
|
||||
|
||||
public function lastExecution(): HasOne
|
||||
{
|
||||
return $this->hasOne(CommandExecution::class)->latest();
|
||||
}
|
||||
}
|
83
app/Models/CommandExecution.php
Normal file
83
app/Models/CommandExecution.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\CommandExecutionStatus;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $command_id
|
||||
* @property int $server_id
|
||||
* @property int $user_id
|
||||
* @property ?int $server_log_id
|
||||
* @property array $variables
|
||||
* @property string $status
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property Command $command
|
||||
* @property ?ServerLog $serverLog
|
||||
* @property Server $server
|
||||
* @property ?User $user
|
||||
*/
|
||||
class CommandExecution extends AbstractModel
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'command_id',
|
||||
'server_id',
|
||||
'user_id',
|
||||
'server_log_id',
|
||||
'variables',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'command_id' => 'integer',
|
||||
'server_id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'server_log_id' => 'integer',
|
||||
'variables' => 'array',
|
||||
];
|
||||
|
||||
public static array $statusColors = [
|
||||
CommandExecutionStatus::EXECUTING => 'warning',
|
||||
CommandExecutionStatus::COMPLETED => 'success',
|
||||
CommandExecutionStatus::FAILED => 'danger',
|
||||
];
|
||||
|
||||
public function command(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Command::class);
|
||||
}
|
||||
|
||||
public function getContent(): string
|
||||
{
|
||||
$content = $this->command->command;
|
||||
foreach ($this->variables as $variable => $value) {
|
||||
if (is_string($value) && ! empty($value)) {
|
||||
$content = str_replace('${'.$variable.'}', $value, $content);
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
public function serverLog(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ServerLog::class);
|
||||
}
|
||||
|
||||
public function server(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
@ -41,6 +41,7 @@
|
||||
* @property Server $server
|
||||
* @property ServerLog[] $logs
|
||||
* @property Deployment[] $deployments
|
||||
* @property Command[] $commands
|
||||
* @property ?GitHook $gitHook
|
||||
* @property DeploymentScript $deploymentScript
|
||||
* @property Queue[] $queues
|
||||
@ -144,6 +145,11 @@ public function deployments(): HasMany
|
||||
return $this->hasMany(Deployment::class);
|
||||
}
|
||||
|
||||
public function commands(): HasMany
|
||||
{
|
||||
return $this->hasMany(Command::class);
|
||||
}
|
||||
|
||||
public function gitHook(): HasOne
|
||||
{
|
||||
return $this->hasOne(GitHook::class);
|
||||
|
61
app/Policies/CommandPolicy.php
Normal file
61
app/Policies/CommandPolicy.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Enums\SiteFeature;
|
||||
use App\Models\Command;
|
||||
use App\Models\Server;
|
||||
use App\Models\Site;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class CommandPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny(User $user, Site $site, Server $server): bool
|
||||
{
|
||||
return ($user->isAdmin() || $server->project->users->contains($user)) &&
|
||||
$server->isReady() &&
|
||||
$site->hasFeature(SiteFeature::COMMANDS) &&
|
||||
$site->isReady();
|
||||
}
|
||||
|
||||
public function view(User $user, Command $command, Site $site, Server $server): bool
|
||||
{
|
||||
return ($user->isAdmin() || $server->project->users->contains($user)) &&
|
||||
$site->server_id === $server->id &&
|
||||
$server->isReady() &&
|
||||
$site->isReady() &&
|
||||
$site->hasFeature(SiteFeature::COMMANDS) &&
|
||||
$command->site_id === $site->id;
|
||||
}
|
||||
|
||||
public function create(User $user, Site $site, Server $server): bool
|
||||
{
|
||||
return ($user->isAdmin() || $server->project->users->contains($user)) &&
|
||||
$server->isReady() &&
|
||||
$site->hasFeature(SiteFeature::COMMANDS) &&
|
||||
$site->isReady();
|
||||
}
|
||||
|
||||
public function update(User $user, Command $command, Site $site, Server $server): bool
|
||||
{
|
||||
return ($user->isAdmin() || $server->project->users->contains($user)) &&
|
||||
$site->server_id === $server->id &&
|
||||
$server->isReady() &&
|
||||
$site->isReady() &&
|
||||
$site->hasFeature(SiteFeature::COMMANDS) &&
|
||||
$command->site_id === $site->id;
|
||||
}
|
||||
|
||||
public function delete(User $user, Command $command, Site $site, Server $server): bool
|
||||
{
|
||||
return ($user->isAdmin() || $server->project->users->contains($user)) &&
|
||||
$site->server_id === $server->id &&
|
||||
$server->isReady() &&
|
||||
$site->isReady() &&
|
||||
$site->hasFeature(SiteFeature::COMMANDS) &&
|
||||
$command->site_id === $site->id;
|
||||
}
|
||||
}
|
@ -37,6 +37,11 @@ public function editRules(array $input): array
|
||||
return $this->createRules($input);
|
||||
}
|
||||
|
||||
public function baseCommands(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function progress(int $percentage): void
|
||||
{
|
||||
$this->site->progress = $percentage;
|
||||
|
54
app/SiteTypes/Laravel.php
Executable file → Normal file
54
app/SiteTypes/Laravel.php
Executable file → Normal file
@ -2,4 +2,56 @@
|
||||
|
||||
namespace App\SiteTypes;
|
||||
|
||||
class Laravel extends PHPSite {}
|
||||
class Laravel extends PHPSite
|
||||
{
|
||||
public function baseCommands(): array
|
||||
{
|
||||
return array_merge(parent::baseCommands(), [
|
||||
// Initial Setup Commands
|
||||
[
|
||||
'name' => 'Generate Application Key',
|
||||
'command' => 'php artisan key:generate',
|
||||
],
|
||||
[
|
||||
'name' => 'Create Storage Symbolic Link',
|
||||
'command' => 'php artisan storage:link',
|
||||
],
|
||||
// Database Commands
|
||||
[
|
||||
'name' => 'Run Database Migrations',
|
||||
'command' => 'php artisan migrate --force',
|
||||
],
|
||||
// Cache & Optimization Commands
|
||||
[
|
||||
'name' => 'Optimize Application',
|
||||
'command' => 'php artisan optimize',
|
||||
],
|
||||
[
|
||||
'name' => 'Clear All Application Optimizations',
|
||||
'command' => 'php artisan optimize:clear',
|
||||
],
|
||||
[
|
||||
'name' => 'Clear Application Cache Only',
|
||||
'command' => 'php artisan cache:clear',
|
||||
],
|
||||
// Queue Commands
|
||||
[
|
||||
'name' => 'Restart Queue Workers',
|
||||
'command' => 'php artisan queue:restart',
|
||||
],
|
||||
[
|
||||
'name' => 'Clear All Failed Queue Jobs',
|
||||
'command' => 'php artisan queue:flush',
|
||||
],
|
||||
// Application State Commands
|
||||
[
|
||||
'name' => 'Put Application in Maintenance Mode',
|
||||
'command' => 'php artisan down --retry=5 --refresh=6 --quiet',
|
||||
],
|
||||
[
|
||||
'name' => 'Bring Application Online',
|
||||
'command' => 'php artisan up',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ public function supportedFeatures(): array
|
||||
{
|
||||
return [
|
||||
SiteFeature::DEPLOYMENT,
|
||||
SiteFeature::COMMANDS,
|
||||
SiteFeature::ENV,
|
||||
SiteFeature::SSL,
|
||||
SiteFeature::QUEUES,
|
||||
@ -55,4 +56,9 @@ public function install(): void
|
||||
$this->progress(65);
|
||||
$this->site->php()?->restart();
|
||||
}
|
||||
|
||||
public function baseCommands(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -21,12 +21,23 @@ public function supportedFeatures(): array
|
||||
{
|
||||
return [
|
||||
SiteFeature::DEPLOYMENT,
|
||||
SiteFeature::COMMANDS,
|
||||
SiteFeature::ENV,
|
||||
SiteFeature::SSL,
|
||||
SiteFeature::QUEUES,
|
||||
];
|
||||
}
|
||||
|
||||
public function baseCommands(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => 'Install Composer Dependencies',
|
||||
'command' => 'composer install --no-dev --no-interaction --no-progress',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function createRules(array $input): array
|
||||
{
|
||||
return [
|
||||
|
@ -19,4 +19,6 @@ public function install(): void;
|
||||
public function editRules(array $input): array;
|
||||
|
||||
public function edit(): void;
|
||||
|
||||
public function baseCommands(): array;
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ public function supportedFeatures(): array
|
||||
{
|
||||
return [
|
||||
SiteFeature::SSL,
|
||||
SiteFeature::COMMANDS,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -56,6 +56,10 @@ public function getWidgets(): array
|
||||
}
|
||||
|
||||
if ($this->site->isReady()) {
|
||||
if (in_array(SiteFeature::COMMANDS, $this->site->type()->supportedFeatures())) {
|
||||
$widgets[] = [Widgets\Commands::class, ['site' => $this->site]];
|
||||
}
|
||||
|
||||
if (in_array(SiteFeature::DEPLOYMENT, $this->site->type()->supportedFeatures())) {
|
||||
$widgets[] = [Widgets\DeploymentsList::class, ['site' => $this->site]];
|
||||
}
|
||||
|
176
app/Web/Pages/Servers/Sites/Widgets/Commands.php
Normal file
176
app/Web/Pages/Servers/Sites/Widgets/Commands.php
Normal file
@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Widgets;
|
||||
|
||||
use App\Actions\Site\CreateCommand;
|
||||
use App\Actions\Site\EditCommand;
|
||||
use App\Actions\Site\ExecuteCommand;
|
||||
use App\Models\Command;
|
||||
use App\Models\CommandExecution;
|
||||
use App\Models\Site;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
class Commands extends Widget
|
||||
{
|
||||
public Site $site;
|
||||
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Command::query()->where('site_id', $this->site->id);
|
||||
}
|
||||
|
||||
protected function applySortingToTableQuery(Builder $query): Builder
|
||||
{
|
||||
return $query->latest('created_at');
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('name'),
|
||||
TextColumn::make('lastExecution.status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (Command $record) => CommandExecution::$statusColors[$record->lastExecution?->status])
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('Last Execution At')
|
||||
->formatStateUsing(fn (Command $record) => $record->lastExecution?->created_at_by_timezone)
|
||||
->searchable()
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getTableHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('new-command')
|
||||
->label('Create a Command')
|
||||
->modalDescription('The command will be executed inside the site\'s directory')
|
||||
->icon('heroicon-o-plus')
|
||||
->authorize(fn () => auth()->user()->can('create', [Command::class, $this->site, $this->site->server]))
|
||||
->action(function (array $data) {
|
||||
run_action($this, function () use ($data) {
|
||||
app(CreateCommand::class)->create($this->site, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Command created!')
|
||||
->send();
|
||||
});
|
||||
})
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->rules(CreateCommand::rules()['name']),
|
||||
TextInput::make('command')
|
||||
->placeholder('php artisan my:command')
|
||||
->rules(CreateCommand::rules()['command'])
|
||||
->helperText('You can use variables like ${VARIABLE_NAME} in the command. The variables will be asked when executing the command'),
|
||||
])
|
||||
->modalSubmitActionLabel('Create')
|
||||
->modalHeading('New Command')
|
||||
->modalWidth('md'),
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->query($this->getTableQuery())
|
||||
->headerActions($this->getTableHeaderActions())
|
||||
->columns($this->getTableColumns())
|
||||
->heading('Commands')
|
||||
->defaultPaginationPageOption(5)
|
||||
->searchable(false)
|
||||
->actions([
|
||||
Action::make('execute')
|
||||
->hiddenLabel()
|
||||
->tooltip('Execute')
|
||||
->icon('heroicon-o-play')
|
||||
->modalWidth(MaxWidth::Medium)
|
||||
->modalSubmitActionLabel('Execute')
|
||||
->form(function (Command $record) {
|
||||
$form = [
|
||||
TextInput::make('command')->default($record->command)->disabled(),
|
||||
];
|
||||
|
||||
foreach ($record->getVariables() as $variable) {
|
||||
$form[] = TextInput::make('variables.'.$variable)
|
||||
->label($variable)
|
||||
->rules(fn (Get $get) => ExecuteCommand::rules($get())['variables.*']);
|
||||
}
|
||||
|
||||
return $form;
|
||||
})
|
||||
->authorize(fn (Command $record) => auth()->user()->can('update', [$record->site, $record->site->server]))
|
||||
->action(function (array $data, Command $record) {
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
app(ExecuteCommand::class)->execute($record, $user, $data);
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
Action::make('logs')
|
||||
->hiddenLabel()
|
||||
->tooltip('Last Log')
|
||||
->icon('heroicon-o-eye')
|
||||
->modalHeading('View Last Execution Log')
|
||||
->modalContent(function (Command $record) {
|
||||
return view('components.console-view', [
|
||||
'slot' => $record->lastExecution?->serverLog?->getContent() ?? 'Not executed yet',
|
||||
'attributes' => new ComponentAttributeBag,
|
||||
]);
|
||||
})
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelActionLabel('Close'),
|
||||
EditAction::make('edit')
|
||||
->hiddenLabel()
|
||||
->tooltip('Edit')
|
||||
->modalHeading('Edit Command')
|
||||
->mutateRecordDataUsing(function (array $data, Command $record) {
|
||||
return [
|
||||
'name' => $record->name,
|
||||
'command' => $record->command,
|
||||
];
|
||||
})
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->rules(EditCommand::rules()['name']),
|
||||
TextInput::make('command')
|
||||
->rules(EditCommand::rules()['command'])
|
||||
->helperText('You can use variables like ${VARIABLE_NAME} in the command. The variables will be asked when executing the command'),
|
||||
|
||||
])
|
||||
->authorize(fn (Command $record) => auth()->user()->can('update', [$record, $this->site, $this->site->server]))
|
||||
->using(function (array $data, Command $record) {
|
||||
app(EditCommand::class)->edit($record, $data);
|
||||
$this->dispatch('$refresh');
|
||||
})
|
||||
->modalWidth(MaxWidth::Medium),
|
||||
DeleteAction::make('delete')
|
||||
->icon('heroicon-o-trash')
|
||||
->hiddenLabel()
|
||||
->tooltip('Delete')
|
||||
->modalHeading('Delete Command')
|
||||
->authorize(fn (Command $record) => auth()->user()->can('delete', [$record, $this->site, $this->site->server]))
|
||||
->using(function (array $data, Command $record) {
|
||||
$record->delete();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
26
database/factories/CommandExecutionFactory.php
Normal file
26
database/factories/CommandExecutionFactory.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\CommandExecutionStatus;
|
||||
use App\Models\Command;
|
||||
use App\Models\CommandExecution;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class CommandExecutionFactory extends Factory
|
||||
{
|
||||
protected $model = CommandExecution::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'command_id' => Command::factory(),
|
||||
'status' => $this->faker->randomElement([
|
||||
CommandExecutionStatus::COMPLETED,
|
||||
CommandExecutionStatus::FAILED,
|
||||
CommandExecutionStatus::EXECUTING,
|
||||
]),
|
||||
'variables' => [],
|
||||
];
|
||||
}
|
||||
}
|
24
database/factories/CommandFactory.php
Normal file
24
database/factories/CommandFactory.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Command;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class CommandFactory extends Factory
|
||||
{
|
||||
protected $model = Command::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->words(3, true),
|
||||
'command' => 'php artisan '.$this->faker->word,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
'site_id' => Site::factory(),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('commands', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedInteger('site_id');
|
||||
$table->string('name');
|
||||
$table->text('command');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('commands');
|
||||
}
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('command_executions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('command_id');
|
||||
$table->unsignedInteger('server_id');
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->unsignedBigInteger('server_log_id')->nullable();
|
||||
$table->json('variables')->nullable();
|
||||
$table->string('status');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('command_executions');
|
||||
}
|
||||
};
|
130
tests/Feature/CommandsTest.php
Normal file
130
tests/Feature/CommandsTest.php
Normal file
@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Facades\SSH;
|
||||
use App\Web\Pages\Servers\Sites\View;
|
||||
use App\Web\Pages\Servers\Sites\Widgets\Commands;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CommandsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_see_commands(): void
|
||||
{
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$this->get(
|
||||
View::getUrl([
|
||||
'server' => $this->server,
|
||||
'site' => $this->site,
|
||||
])
|
||||
)
|
||||
->assertSuccessful()
|
||||
->assertSee($this->site->domain)
|
||||
->assertSee('Commands');
|
||||
}
|
||||
|
||||
public function test_create_command(): void
|
||||
{
|
||||
$this->actingAs($this->user);
|
||||
|
||||
Livewire::test(Commands::class, ['site' => $this->site])
|
||||
->assertTableHeaderActionsExistInOrder(['new-command'])
|
||||
->callTableAction('new-command', null, [
|
||||
'name' => 'Test Command',
|
||||
'command' => 'echo "${MESSAGE}"',
|
||||
])
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseHas('commands', [
|
||||
'site_id' => $this->site->id,
|
||||
'name' => 'Test Command',
|
||||
'command' => 'echo "${MESSAGE}"',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_edit_command(): void
|
||||
{
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$command = $this->site->commands()->create([
|
||||
'name' => 'Test Command',
|
||||
'command' => 'echo "${MESSAGE}"',
|
||||
]);
|
||||
|
||||
Livewire::test(Commands::class, ['site' => $this->site])
|
||||
->callTableAction('edit', $command->id, [
|
||||
'name' => 'Updated Command',
|
||||
'command' => 'ls -la',
|
||||
])
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseHas('commands', [
|
||||
'id' => $command->id,
|
||||
'site_id' => $this->site->id,
|
||||
'name' => 'Updated Command',
|
||||
'command' => 'ls -la',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_delete_command(): void
|
||||
{
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$command = $this->site->commands()->create([
|
||||
'name' => 'Test Command',
|
||||
'command' => 'echo "${MESSAGE}"',
|
||||
]);
|
||||
|
||||
Livewire::test(Commands::class, ['site' => $this->site])
|
||||
->callTableAction('delete', $command->id)
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseMissing('commands', [
|
||||
'id' => $command->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_execute_command(): void
|
||||
{
|
||||
SSH::fake('echo "Hello, world!"');
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$command = $this->site->commands()->create([
|
||||
'name' => 'Test Command',
|
||||
'command' => 'echo "${MESSAGE}"',
|
||||
]);
|
||||
|
||||
Livewire::test(Commands::class, ['site' => $this->site])
|
||||
->callTableAction('execute', $command->id, [
|
||||
'variables' => [
|
||||
'MESSAGE' => 'Hello, world!',
|
||||
],
|
||||
])
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseHas('command_executions', [
|
||||
'command_id' => $command->id,
|
||||
'variables' => json_encode(['MESSAGE' => 'Hello, world!']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_execute_command_validation_error(): void
|
||||
{
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$command = $this->site->commands()->create([
|
||||
'name' => 'Test Command',
|
||||
'command' => 'echo "${MESSAGE}"',
|
||||
]);
|
||||
|
||||
Livewire::test(Commands::class, ['site' => $this->site])
|
||||
->callTableAction('execute', $command->id, [])
|
||||
->assertHasActionErrors();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user