diff --git a/app/Bots/GenericApi.php b/app/Bots/GenericApi.php new file mode 100644 index 0000000..a53b5d9 --- /dev/null +++ b/app/Bots/GenericApi.php @@ -0,0 +1,82 @@ +config = $config; + } + + public function run(): void + { + try { + $this->client = new Client(); + + $options = []; + + if (!empty($this->config['headers'])) { + $options['headers'] = json_decode($this->config['headers'], true); + } + + if (!empty($this->config['body'])) { + $options['json'] = json_decode($this->config['body'], true); + } + + $this->client->request( + $this->config['method'] ?? 'GET', + $this->config['url'], + $options + ); + } catch (GuzzleException $e) { + Log::error("Call to {$this->config['url']} failed" . $e->getMessage()); + } + } + + public static function configSchema(): array + { + return [ + 'url' => [ + 'type' => 'string', + 'label' => 'API URL', + 'rules' => [ + 'required', + 'url', + ] + ], + 'method' => [ + 'type' => 'string', + 'label' => 'HTTP Method', + 'rules' => [ + 'required', + 'in:GET,POST,PUT,DELETE,PATCH', + ] + ], + 'headers' => [ + 'type' => 'json', + 'label' => 'Request Headers (JSON)', + 'rules' => [ + 'nullable', + 'json', + ] + ], + 'body' => [ + 'type' => 'json', + 'label' => 'Request Body (JSON)', + 'rules' => [ + 'nullable', + 'json', + ] + ], + ]; + } +} diff --git a/app/Bots/Mattermost.php b/app/Bots/Mattermost.php index 091e99f..891f5e4 100644 --- a/app/Bots/Mattermost.php +++ b/app/Bots/Mattermost.php @@ -4,6 +4,9 @@ namespace App\Bots; use App\Bots\BotContract; use GuzzleHttp\Client; +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\RequestOptions; use Illuminate\Support\Facades\Log; class Mattermost implements BotContract @@ -25,49 +28,48 @@ class Mattermost implements BotContract public function run(): void { try { - $request = new \GuzzleHttp\Psr7\Request( + $this->client->request( $this->config['method'] ?? 'POST', $this->config['endpoint'], - ['Content-Type' => 'application/json'], - json_encode($this->config['body'] ?? []) + [ + 'json' => json_decode($this->config['body'] ?? '', true), + ] ); - - $res = $this->client->send($request); - - // $res = $this->client->put('users/me/status/custom', [ - // 'json' => [ - // 'emoji' => 'house', - // 'text' => 'Working Home', - // 'status' => 'online', - // ] - // ]); - - if ($res->getStatusCode() === 200) { - Log::info("Mattermost home status updated successfully."); - } else { - Log::error("Failed to update Mattermost status. HTTP Status: " . $res->getStatusCode()); - } - } catch (\Exception $e) { - Log::error("Error updating Mattermost status: " . $e->getMessage()); + } catch (GuzzleException $e) { + Log::error("Call to Mattermost failed. " . $e->getMessage()); } } public static function configSchema(): array { return [ - 'endpoint' => ['type' => 'string', 'label' => 'API Endpoint', 'rules' => [ - 'required', - 'string', - 'max:255', - ]], - 'method' => ['type' => 'string', 'label' => 'HTTP Method', 'default' => 'POST', 'rules' => [ - 'required', - 'in:GET,POST,PUT,DELETE,PATCH', - ]], - 'body' => ['type' => 'array', 'label' => 'Request Body (JSON)', 'rules' => [ - 'nullable', - 'array', - ]], + 'endpoint' => [ + 'type' => 'string', + 'label' => 'API Endpoint', + 'rules' => [ + 'required', + 'string', + 'max:255', + ] + ], + 'method' => [ + 'type' => 'string', + 'label' => 'HTTP Method', + 'default' => 'POST', + 'rules' => [ + 'required', + 'in:GET,POST,PUT,DELETE,PATCH', + ] + ], + 'body' => [ + 'type' => 'json', + 'label' => 'Request Body (JSON)', + 'rules' => [ + 'nullable', + 'string', + 'json', + ] + ], ]; } } diff --git a/app/Bots/Mattermost/RemoveStatus.php b/app/Bots/Mattermost/RemoveStatus.php deleted file mode 100644 index 2103e37..0000000 --- a/app/Bots/Mattermost/RemoveStatus.php +++ /dev/null @@ -1,46 +0,0 @@ -client = new Client([ - 'base_uri' => rtrim(config('scheduler.mattermost.server_url'), '/') . '/api/v4/', - 'headers' => [ - 'Authorization' => 'Bearer ' . config('scheduler.mattermost.access_token'), - ], - ]); - } - - public function run(): void - { - try { - $res = $this->client->delete('users/me/status/custom'); - - if ($res->getStatusCode() === 200) { - Log::info("Mattermost status cleared"); - } else { - Log::error("Failed to update Mattermost status. HTTP Status: " . $res->getStatusCode()); - } - } catch (\Exception $e) { - Log::error("Error updating Mattermost status: " . $e->getMessage()); - } - } - - public static function configSchema(): array - { - return [ - 'endpoint' => ['type' => 'string', 'label' => 'API Endpoint'], - 'method' => ['type' => 'string', 'label' => 'HTTP Method', 'default' => 'POST'], - 'body' => ['type' => 'array', 'label' => 'Request Body (JSON)'], - ]; - } -} diff --git a/app/Bots/Mattermost/WednesdayHomeStatus.php b/app/Bots/Mattermost/WednesdayHomeStatus.php deleted file mode 100644 index 168ad91..0000000 --- a/app/Bots/Mattermost/WednesdayHomeStatus.php +++ /dev/null @@ -1,54 +0,0 @@ -client = new Client([ - 'base_uri' => rtrim(config('scheduler.mattermost.server_url'), '/') . '/api/v4/', - 'headers' => [ - 'Authorization' => 'Bearer ' . config('scheduler.mattermost.access_token'), - 'Accept' => 'application/json', - 'Content-Type' => 'application/json', - ], - ]); - } - - public function run(): void - { - try { - $res = $this->client->put('users/me/status/custom', [ - 'json' => [ - 'emoji' => 'house', - 'text' => 'Working Home', - 'status' => 'online', - ] - ]); - - if ($res->getStatusCode() === 200) { - Log::info("Mattermost home status updated successfully."); - } else { - Log::error("Failed to update Mattermost status. HTTP Status: " . $res->getStatusCode()); - } - } catch (\Exception $e) { - Log::error("Error updating Mattermost status: " . $e->getMessage()); - } - } - - public static function configSchema(): array - { - return [ - 'endpoint' => ['type' => 'string', 'label' => 'API Endpoint'], - 'method' => ['type' => 'string', 'label' => 'HTTP Method', 'default' => 'POST'], - 'body' => ['type' => 'array', 'label' => 'Request Body (JSON)'], - ]; - } -} diff --git a/app/Livewire/BotsList.php b/app/Livewire/BotsList.php index b7110e1..ce87fa2 100644 --- a/app/Livewire/BotsList.php +++ b/app/Livewire/BotsList.php @@ -2,6 +2,8 @@ namespace App\Livewire; +use App\Models\Bot; +use Jantinnerezo\LivewireAlert\Facades\LivewireAlert; use Livewire\Component; class BotsList extends Component @@ -25,6 +27,47 @@ class BotsList extends Component } } + public function deleteBot(int $botId): void + { + $bot = \App\Models\Bot::find($botId); + + if ($bot) { + LivewireAlert::title('Are you sure you want to delete ' . $bot->name . '?') + ->asConfirm() + ->onConfirm('confirmDelete', [$bot]) + ->show(); + } + } + + public function confirmDelete(Bot $bot): void + { + $bot->delete(); + $this->bots = \App\Models\Bot::all(); // Refresh the list + flash()->success("Bot '{$bot->name}' has been deleted."); + } + + public function runBot($botId) + { + $bot = \App\Models\Bot::find($botId); + + if ($bot) { + LivewireAlert::title('Are you sure you want to run ' . $bot->name . '?') + ->asConfirm() + ->onConfirm('confirmRunBot', [$bot]) + ->show(); + } + } + + public function confirmRunBot(Bot $bot): void + { + // Dispatch the job to run the bot + $class = new $bot->class($bot->config ?? []); + + $class->run(); + + flash()->success("Bot '{$bot->name}' is being executed."); + } + public function render() { return view('livewire.bots-list'); diff --git a/app/Livewire/CreateEditBot.php b/app/Livewire/CreateEditBot.php index 2321f00..13dacb1 100644 --- a/app/Livewire/CreateEditBot.php +++ b/app/Livewire/CreateEditBot.php @@ -3,6 +3,7 @@ namespace App\Livewire; use App\Bots\BotContract; +use App\Models\Bot; use Cron\CronExpression; use Illuminate\Support\Arr; use Illuminate\Support\Facades\File; @@ -14,6 +15,8 @@ use Lorisleiva\CronTranslator\CronTranslator; class CreateEditBot extends Component { + public ?Bot $bot = null; + public string $name = ''; public string $class = ''; @@ -24,7 +27,7 @@ class CreateEditBot extends Component public string $cron_text = 'Every minute'; - public string $config = '{}'; + public array $config = []; public array $classList = []; @@ -32,17 +35,18 @@ class CreateEditBot extends Component public string $routeName = ''; - public function mount(?int $bot = null) + public function mount() { $this->routeName = URL::current() === route('bots.create') ? 'Create Bot' : 'Edit Bot'; - $bot = \App\Models\Bot::find($bot); - if ($bot) { - $this->name = $bot->name; - $this->class = $bot->class; - $this->schedule = $bot->schedule; - $this->enabled = $bot->enabled; - $this->cron_text = \Lorisleiva\CronTranslator\CronTranslator::translate($bot->schedule); + if ($this->bot) { + $this->name = $this->bot->name; + $this->class = $this->bot->class; + $this->config = $this->bot->config ?? []; + $this->schedule = $this->bot->schedule; + $this->enabled = $this->bot->enabled; + $this->cron_text = CronTranslator::translate($this->bot->schedule); + $this->configSchema = $this->bot->class::configSchema(); } $basePath = $basePath ?? app_path('Bots'); @@ -60,7 +64,7 @@ class CreateEditBot extends Component ? $class::label() : class_basename($class); - $this->classList[$label] = $class; + $this->classList[$class] = $label; } } } @@ -119,32 +123,31 @@ class CreateEditBot extends Component return; } - if (URL::current() !== route('bots.create')) { - $bot = \App\Models\Bot::where('name', $this->name)->first(); - if ($bot) { - $bot->update([ - 'name' => $this->name, - 'class' => $this->class, - 'enabled' => $this->enabled, - 'schedule' => $this->schedule, - ]); + if ($this->routeName !== 'Create Bot') { + $this->bot->update([ + 'name' => $this->name, + 'class' => $this->class, + 'enabled' => $this->enabled, + 'schedule' => $this->schedule, + 'config' => $this->config, + ]); - flash()->success('Bot updated successfully.'); + flash()->success('Bot updated successfully.'); - return; - } + return; } else { \App\Models\Bot::create([ 'name' => $this->name, 'class' => $this->class, 'enabled' => $this->enabled, 'schedule' => $this->schedule, + 'config' => $this->config, ]); } flash()->success('Bot created successfully.'); - $this->reset(['name', 'namespace', 'class', 'enabled', 'schedule']); + $this->reset(['name', 'class', 'enabled', 'schedule', 'config', 'configSchema']); } public function updatedSchedule($value) @@ -157,6 +160,34 @@ class CreateEditBot extends Component } } + protected function validationAttributes(): array + { + $attributes = []; + + foreach ($this->configSchema as $field => $meta) { + $attributes["config.$field"] = $meta['label']; + } + + $attributes['name'] = 'Bot Name'; + $attributes['schedule'] = 'Schedule'; + $attributes['class'] = 'Bot Schema'; + + return $attributes; + } + + public function prettify(string $field): void + { + if (isset($this->config[$field])) { + if (json_validate($this->config[$field])) { + $decoded = json_decode($this->config[$field], true); + $this->config[$field] = json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $this->resetErrorBag("config.$field"); + } else { + $this->addError("config.$field", 'Invalid JSON format.'); + } + } + } + public function render() { return view('livewire.create-edit-bot'); diff --git a/app/Models/Bot.php b/app/Models/Bot.php index 31462c4..c1829b2 100644 --- a/app/Models/Bot.php +++ b/app/Models/Bot.php @@ -31,9 +31,17 @@ class Bot extends Model ); } - public function cronToHuman(): Attribute + public function cronToHuman(): Attribute { return Attribute::make( - get: fn () => CronTranslator::translate($this->schedule)); + get: fn() => CronTranslator::translate($this->schedule) + ); + } + + public function className(): Attribute + { + return Attribute::make( + get: fn() => class_basename($this->class) + ); } } diff --git a/app/Models/Scripts.php b/app/Models/Scripts.php new file mode 100644 index 0000000..29d84d8 --- /dev/null +++ b/app/Models/Scripts.php @@ -0,0 +1,10 @@ +id(); + $table->string('name'); + $table->string('script'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('scripts'); + } +}; diff --git a/package-lock.json b/package-lock.json index 7b6e205..8ab7f34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "concurrently": "^9.0.1", "laravel-vite-plugin": "^2.0", "prettier-plugin-blade": "^2.1.21", + "sweetalert2": "^11.22.5", "tailwindcss": "^4.0.7", "vite": "^7.0.4" }, @@ -2220,6 +2221,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/sweetalert2": { + "version": "11.22.5", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.5.tgz", + "integrity": "sha512-k9gb2M0n4b830FaWDmqaFQULIRvKixTbJOBkTN5KwRNIT8UxjGxusC9g67cj8sCxkJb9nVy2+PgyVd7vYK7cug==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/limonte" + } + }, "node_modules/tailwindcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", diff --git a/package.json b/package.json index 70f18a5..b2251b9 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "concurrently": "^9.0.1", "laravel-vite-plugin": "^2.0", "prettier-plugin-blade": "^2.1.21", + "sweetalert2": "^11.22.5", "tailwindcss": "^4.0.7", "vite": "^7.0.4" }, diff --git a/resources/js/app.js b/resources/js/app.js index e69de29..3b49927 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -0,0 +1,3 @@ +import Swal from 'sweetalert2'; + +window.Swal = Swal diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 29bfc74..2f30e47 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -1,10 +1,12 @@
-
@@ -14,6 +16,11 @@

Active Bots

+ @foreach ($bots->where('enabled', 1) as $bot) +
+

{{ $bot->name }}

+
+ @endforeach
Name - Class + Schema Schedule Next due Enabled + Actions @foreach ($bots as $bot) - - + + {{ $bot->name }} - - {{ $bot->class }} - + + {{ $bot->class_name }} + {{ $bot->schedule }} ({{ $bot->cron_to_human }}) - {{ $bot->next_due }} - + {{ $bot->next_due }} + enabled) wire:click="toggleBot({{ $bot->id }})" /> + + + + + @endforeach diff --git a/resources/views/livewire/create-edit-bot.blade.php b/resources/views/livewire/create-edit-bot.blade.php index fa1b7da..bbc6bff 100644 --- a/resources/views/livewire/create-edit-bot.blade.php +++ b/resources/views/livewire/create-edit-bot.blade.php @@ -1,3 +1,4 @@ +@section('title', $routeName)
- @error('bot') + @error('class') {{ $message }} @enderror
@@ -41,20 +42,50 @@
-

Config

+
+

Config

+
@forelse ($configSchema as $field => $meta) - - config[$field] ?? '') }}" - /> +
+ + + @if ($meta['type'] === 'json') +
+ + +
+ + Prettify + + @else + config[$field] ?? '') }}" + /> + @endif +
@error('config.' . $field) {{ $message }} @enderror @empty - Select a bot first + Select a bot first @endforelse
@@ -68,9 +99,56 @@
+ + diff --git a/resources/views/partials/head.blade.php b/resources/views/partials/head.blade.php index dce8058..c473b22 100644 --- a/resources/views/partials/head.blade.php +++ b/resources/views/partials/head.blade.php @@ -1,14 +1,19 @@ -{{ $title ?? config('app.name') }} + + {{ config('app.name', 'VETiSearch') }} - @yield('title', $title ?? '') + - - - - - - + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) @fluxAppearance