Add generic api schema and scripts model

This commit is contained in:
2025-08-30 18:20:24 +02:00
parent 774d1cf45f
commit f6993d463d
18 changed files with 493 additions and 192 deletions

82
app/Bots/GenericApi.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
namespace App\Bots;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Log;
class GenericApi implements BotContract
{
protected array $config;
protected Client $client;
public function __construct(array $config = [])
{
$this->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',
]
],
];
}
}

View File

@@ -4,6 +4,9 @@ namespace App\Bots;
use App\Bots\BotContract; use App\Bots\BotContract;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\RequestOptions;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
class Mattermost implements BotContract class Mattermost implements BotContract
@@ -25,49 +28,48 @@ class Mattermost implements BotContract
public function run(): void public function run(): void
{ {
try { try {
$request = new \GuzzleHttp\Psr7\Request( $this->client->request(
$this->config['method'] ?? 'POST', $this->config['method'] ?? 'POST',
$this->config['endpoint'], $this->config['endpoint'],
['Content-Type' => 'application/json'], [
json_encode($this->config['body'] ?? []) 'json' => json_decode($this->config['body'] ?? '', true),
]
); );
} catch (GuzzleException $e) {
$res = $this->client->send($request); Log::error("Call to Mattermost failed. " . $e->getMessage());
// $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 public static function configSchema(): array
{ {
return [ return [
'endpoint' => ['type' => 'string', 'label' => 'API Endpoint', 'rules' => [ 'endpoint' => [
'type' => 'string',
'label' => 'API Endpoint',
'rules' => [
'required', 'required',
'string', 'string',
'max:255', 'max:255',
]], ]
'method' => ['type' => 'string', 'label' => 'HTTP Method', 'default' => 'POST', 'rules' => [ ],
'method' => [
'type' => 'string',
'label' => 'HTTP Method',
'default' => 'POST',
'rules' => [
'required', 'required',
'in:GET,POST,PUT,DELETE,PATCH', 'in:GET,POST,PUT,DELETE,PATCH',
]], ]
'body' => ['type' => 'array', 'label' => 'Request Body (JSON)', 'rules' => [ ],
'body' => [
'type' => 'json',
'label' => 'Request Body (JSON)',
'rules' => [
'nullable', 'nullable',
'array', 'string',
]], 'json',
]
],
]; ];
} }
} }

View File

@@ -1,46 +0,0 @@
<?php
namespace App\Bots\Mattermost;
use App\Bots\BotContract;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
class RemoveStatus implements BotContract
{
protected Client $client;
public function __construct(private ?array $config = [])
{
$this->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)'],
];
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Bots\Mattermost;
use App\Bots\BotContract;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
class WednesdayHomeStatus implements BotContract
{
protected Client $client;
public function __construct(private ?array $config = [])
{
$this->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)'],
];
}
}

View File

@@ -2,6 +2,8 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\Bot;
use Jantinnerezo\LivewireAlert\Facades\LivewireAlert;
use Livewire\Component; use Livewire\Component;
class BotsList extends 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() public function render()
{ {
return view('livewire.bots-list'); return view('livewire.bots-list');

View File

@@ -3,6 +3,7 @@
namespace App\Livewire; namespace App\Livewire;
use App\Bots\BotContract; use App\Bots\BotContract;
use App\Models\Bot;
use Cron\CronExpression; use Cron\CronExpression;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
@@ -14,6 +15,8 @@ use Lorisleiva\CronTranslator\CronTranslator;
class CreateEditBot extends Component class CreateEditBot extends Component
{ {
public ?Bot $bot = null;
public string $name = ''; public string $name = '';
public string $class = ''; public string $class = '';
@@ -24,7 +27,7 @@ class CreateEditBot extends Component
public string $cron_text = 'Every minute'; public string $cron_text = 'Every minute';
public string $config = '{}'; public array $config = [];
public array $classList = []; public array $classList = [];
@@ -32,17 +35,18 @@ class CreateEditBot extends Component
public string $routeName = ''; public string $routeName = '';
public function mount(?int $bot = null) public function mount()
{ {
$this->routeName = URL::current() === route('bots.create') ? 'Create Bot' : 'Edit Bot'; $this->routeName = URL::current() === route('bots.create') ? 'Create Bot' : 'Edit Bot';
$bot = \App\Models\Bot::find($bot); if ($this->bot) {
if ($bot) { $this->name = $this->bot->name;
$this->name = $bot->name; $this->class = $this->bot->class;
$this->class = $bot->class; $this->config = $this->bot->config ?? [];
$this->schedule = $bot->schedule; $this->schedule = $this->bot->schedule;
$this->enabled = $bot->enabled; $this->enabled = $this->bot->enabled;
$this->cron_text = \Lorisleiva\CronTranslator\CronTranslator::translate($bot->schedule); $this->cron_text = CronTranslator::translate($this->bot->schedule);
$this->configSchema = $this->bot->class::configSchema();
} }
$basePath = $basePath ?? app_path('Bots'); $basePath = $basePath ?? app_path('Bots');
@@ -60,7 +64,7 @@ class CreateEditBot extends Component
? $class::label() ? $class::label()
: class_basename($class); : class_basename($class);
$this->classList[$label] = $class; $this->classList[$class] = $label;
} }
} }
} }
@@ -119,32 +123,31 @@ class CreateEditBot extends Component
return; return;
} }
if (URL::current() !== route('bots.create')) { if ($this->routeName !== 'Create Bot') {
$bot = \App\Models\Bot::where('name', $this->name)->first(); $this->bot->update([
if ($bot) {
$bot->update([
'name' => $this->name, 'name' => $this->name,
'class' => $this->class, 'class' => $this->class,
'enabled' => $this->enabled, 'enabled' => $this->enabled,
'schedule' => $this->schedule, 'schedule' => $this->schedule,
'config' => $this->config,
]); ]);
flash()->success('Bot updated successfully.'); flash()->success('Bot updated successfully.');
return; return;
}
} else { } else {
\App\Models\Bot::create([ \App\Models\Bot::create([
'name' => $this->name, 'name' => $this->name,
'class' => $this->class, 'class' => $this->class,
'enabled' => $this->enabled, 'enabled' => $this->enabled,
'schedule' => $this->schedule, 'schedule' => $this->schedule,
'config' => $this->config,
]); ]);
} }
flash()->success('Bot created successfully.'); flash()->success('Bot created successfully.');
$this->reset(['name', 'namespace', 'class', 'enabled', 'schedule']); $this->reset(['name', 'class', 'enabled', 'schedule', 'config', 'configSchema']);
} }
public function updatedSchedule($value) 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() public function render()
{ {
return view('livewire.create-edit-bot'); return view('livewire.create-edit-bot');

View File

@@ -34,6 +34,14 @@ class Bot extends Model
public function cronToHuman(): Attribute public function cronToHuman(): Attribute
{ {
return Attribute::make( 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)
);
} }
} }

10
app/Models/Scripts.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Scripts extends Model
{
//
}

View File

@@ -11,6 +11,7 @@
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"dragonmantank/cron-expression": "^3.4", "dragonmantank/cron-expression": "^3.4",
"jantinnerezo/livewire-alert": "^4.0",
"laravel/framework": "^12.0", "laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1", "laravel/tinker": "^2.10.1",
"livewire/flux": "^2.1.1", "livewire/flux": "^2.1.1",

66
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "0e6fbc1b4cb99ff544ccd78faca75fd6", "content-hash": "3f7408ae004087437312ccf363afe9ba",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@@ -1053,6 +1053,70 @@
], ],
"time": "2025-08-22T14:27:06+00:00" "time": "2025-08-22T14:27:06+00:00"
}, },
{
"name": "jantinnerezo/livewire-alert",
"version": "v4.0.6",
"source": {
"type": "git",
"url": "https://github.com/jantinnerezo/livewire-alert.git",
"reference": "5e0eeeb5eeec6429fc49eb66ecf1e6898b7e25c6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jantinnerezo/livewire-alert/zipball/5e0eeeb5eeec6429fc49eb66ecf1e6898b7e25c6",
"reference": "5e0eeeb5eeec6429fc49eb66ecf1e6898b7e25c6",
"shasum": ""
},
"require": {
"illuminate/support": "^10.0|^11.0|^12.0",
"livewire/livewire": "^3.0",
"php": "^8.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.70",
"orchestra/testbench": "^8.0|^9.0|^10.0",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^9.5|^10.0|^11.5.3"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"LivewireAlert": "Jantinnerezo\\LivewireAlert\\LivewireAlert"
},
"providers": [
"Jantinnerezo\\LivewireAlert\\LivewireAlertServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Jantinnerezo\\LivewireAlert\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jantinn Erezo",
"email": "erezojantinn@gmail.com",
"role": "Developer"
}
],
"description": "This package provides a simple alert utilities for your livewire components.",
"homepage": "https://github.com/jantinnerezo/livewire-alert",
"keywords": [
"jantinnerezo",
"livewire-alert"
],
"support": {
"issues": "https://github.com/jantinnerezo/livewire-alert/issues",
"source": "https://github.com/jantinnerezo/livewire-alert/tree/v4.0.6"
},
"time": "2025-07-13T10:49:59+00:00"
},
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v12.26.4", "version": "v12.26.4",

View File

@@ -0,0 +1,29 @@
<?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('scripts', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('script');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('scripts');
}
};

11
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0", "laravel-vite-plugin": "^2.0",
"prettier-plugin-blade": "^2.1.21", "prettier-plugin-blade": "^2.1.21",
"sweetalert2": "^11.22.5",
"tailwindcss": "^4.0.7", "tailwindcss": "^4.0.7",
"vite": "^7.0.4" "vite": "^7.0.4"
}, },
@@ -2220,6 +2221,16 @@
"url": "https://github.com/chalk/supports-color?sponsor=1" "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": { "node_modules/tailwindcss": {
"version": "4.1.11", "version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",

View File

@@ -12,6 +12,7 @@
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0", "laravel-vite-plugin": "^2.0",
"prettier-plugin-blade": "^2.1.21", "prettier-plugin-blade": "^2.1.21",
"sweetalert2": "^11.22.5",
"tailwindcss": "^4.0.7", "tailwindcss": "^4.0.7",
"vite": "^7.0.4" "vite": "^7.0.4"
}, },

View File

@@ -0,0 +1,3 @@
import Swal from 'sweetalert2';
window.Swal = Swal

View File

@@ -1,10 +1,12 @@
<x-layouts.app :title="__('Dashboard')"> <x-layouts.app :title="__('Dashboard')">
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl"> <div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
<div class="flex justify-end"> <div class="flex justify-end">
<button class="bg-gray-600 p-2 text-white rounded-lg"> <button
<a href="{{ route('bots.create') }}"> class="bg-gray-600 p-2 text-white rounded-lg cursor-pointer"
wire:navigate
href="{{ route('bots.create') }}"
>
{{ __('Add Bot') }} {{ __('Add Bot') }}
</a>
</button> </button>
</div> </div>
<div class="grid auto-rows-min gap-4 md:grid-cols-3"> <div class="grid auto-rows-min gap-4 md:grid-cols-3">
@@ -14,6 +16,11 @@
<div class="p-4"> <div class="p-4">
<h2>Active Bots</h2> <h2>Active Bots</h2>
</div> </div>
@foreach ($bots->where('enabled', 1) as $bot)
<div class="px-4 py-1">
<p>{{ $bot->name }}</p>
</div>
@endforeach
</div> </div>
<div <div
class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700" class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700"

View File

@@ -3,35 +3,61 @@
<thead> <thead>
<tr> <tr>
<th class="border px-4 py-2">Name</th> <th class="border px-4 py-2">Name</th>
<th class="border px-4 py-2">Class</th> <th class="border px-4 py-2">Schema</th>
<th class="border px-4 py-2">Schedule</th> <th class="border px-4 py-2">Schedule</th>
<th class="border px-4 py-2">Next due</th> <th class="border px-4 py-2">Next due</th>
<th class="border px-4 py-2">Enabled</th> <th class="border px-4 py-2">Enabled</th>
<th class="border px-4 py-2">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody class="text-center"> <tbody class="text-center">
@foreach ($bots as $bot) @foreach ($bots as $bot)
<tr> <tr>
<td class="border px-4 py-2"> <td class="px-4 py-2">
<a href="{{ route('bots.edit', $bot) }}" class="text-blue-500"> <a
wire:navigate
href="{{ route('bots.edit', $bot) }}"
class="text-blue-500"
>
{{ $bot->name }} {{ $bot->name }}
</a> </a>
</td> </td>
<td class="border px-4 py-2">{{ $bot->class }}</td> <td class="px-4 py-2">{{ $bot->class_name }}</td>
<td class="border px-4 py-2"> <td class="px-4 py-2">
{{ $bot->schedule }} {{ $bot->schedule }}
<span class="text-gray-300"> <span class="text-gray-300">
({{ $bot->cron_to_human }}) ({{ $bot->cron_to_human }})
</span> </span>
</td> </td>
<td class="border px-4 py-2">{{ $bot->next_due }}</td> <td class="px-4 py-2">{{ $bot->next_due }}</td>
<td class="border px-4 py-2"> <td class="px-4 py-2">
<input <input
type="checkbox" type="checkbox"
@checked($bot->enabled) @checked($bot->enabled)
wire:click="toggleBot({{ $bot->id }})" wire:click="toggleBot({{ $bot->id }})"
/> />
</td> </td>
<td>
<button
wire:click="runBot({{ $bot->id }})"
class="ml-4 cursor-pointer"
>
<i class="fas fa-play"></i>
</button>
<button
href="{{ route('bots.edit', $bot) }}"
wire:navigate
class="ml-4 cursor-pointer"
>
<i class="fas fa-edit"></i>
</button>
<button
wire:click="deleteBot({{ $bot->id }})"
class="ml-4 cursor-pointer"
>
<i class="fas fa-trash"></i>
</button>
</td>
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@@ -1,3 +1,4 @@
@section('title', $routeName)
<div> <div>
<div class="mb-4"> <div class="mb-4">
<input <input
@@ -14,13 +15,13 @@
<div> <div>
<select wire:model.live="class" class="border p-2 rounded mb-2 w-full"> <select wire:model.live="class" class="border p-2 rounded mb-2 w-full">
<option hidden selected>Select Bot Schema</option> <option hidden selected>Select Bot Schema</option>
@foreach ($classList as $label => $class) @foreach ($classList as $class => $label)
<option class="text-black" value="{{ $label }}"> <option class="text-black" value="{{ $class }}">
{{ $label }} {{ $label }}
</option> </option>
@endforeach @endforeach
</select> </select>
@error('bot') @error('class')
<span class="text-red-500">{{ $message }}</span> <span class="text-red-500">{{ $message }}</span>
@enderror @enderror
</div> </div>
@@ -41,20 +42,50 @@
</div> </div>
<div class="mt-4"> <div class="mt-4">
<div class="mb-4 text-xl">
<h2>Config</h2> <h2>Config</h2>
</div>
@forelse ($configSchema as $field => $meta) @forelse ($configSchema as $field => $meta)
<div class="mb-4">
<label>{{ $meta['label'] }}</label> <label>{{ $meta['label'] }}</label>
@if ($meta['type'] === 'json')
<div x-data="jsonEditor(@entangle('config.' . $field))">
<label
for="config-body"
class="block font-semibold mb-1"
>
Request Body (JSON)
</label>
<textarea
id="config-body"
x-model="value"
wire:model.live="config.{{ $field }}"
@keydown="handleKeydown($event)"
class="w-full h-40 p-2 font-mono border rounded-md"
></textarea>
</div>
<span
class="text-blue-300 cursor-pointer"
wire:click="prettify('{{ $field }}')"
>
Prettify
</span>
@else
<input <input
wire:model.live="config.{{ $field }}"
class="border p-2 rounded mb-2 w-full" class="border p-2 rounded mb-2 w-full"
type="text" type="text"
name="config[{{ $field }}]" name="config[{{ $field }}]"
value="{{ old("config.$field", $bot->config[$field] ?? '') }}" value="{{ old("config.$field", $bot->config[$field] ?? '') }}"
/> />
@endif
</div>
@error('config.' . $field) @error('config.' . $field)
<span class="text-red-500">{{ $message }}</span> <span class="text-red-500">{{ $message }}</span>
@enderror @enderror
@empty @empty
Select a bot first <i class="text-gray-400">Select a bot first</i>
@endforelse @endforelse
</div> </div>
@@ -68,9 +99,56 @@
<div> <div>
<button <button
wire:click="save" wire:click="save"
class="mt-6 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" class="mt-6 bg-blue-500 text-white px-4 py-2 rounded cursor-pointer"
> >
{{ $routeName }} {{ $routeName }}
</button> </button>
</div> </div>
</div> </div>
<script>
function jsonEditor(model) {
return {
value: model,
handleKeydown(e) {
const textarea = e.target;
const val = textarea.value;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
// Map of pairs
const pairs = {
'{': '}',
'[': ']',
'"': '"',
"'": "'",
};
if (pairs[e.key]) {
e.preventDefault();
const open = e.key;
const close = pairs[e.key];
textarea.setRangeText(open + close, start, end, 'end');
textarea.selectionStart = textarea.selectionEnd = start + 1;
this.value = textarea.value;
}
// Auto-indent new line inside braces
if (
e.key === 'Enter' &&
val[start - 1] === '{' &&
val[end] === '}'
) {
e.preventDefault();
const indent = ' ';
const newText = `\n${indent}\n`;
textarea.setRangeText(newText, start, end, 'end');
textarea.selectionStart = textarea.selectionEnd =
start + newText.length - 1;
this.value = textarea.value;
}
},
};
}
</script>

View File

@@ -1,14 +1,19 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ $title ?? config('app.name') }}</title> <title>
{{ config('app.name', 'VETiSearch') }} - @yield('title', $title ?? '')
</title>
<link rel="icon" href="/favicon.ico" sizes="any"> <link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml"> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png"> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />
<link rel="preconnect" href="https://fonts.bunny.net" />
<link
href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600"
rel="stylesheet"
/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.0/css/all.min.css" integrity="sha512-DxV+EoADOkOygM4IR9yXP8Sb2qwgidEmeqAEmDKIOfPRQZOWbXCzLC6vjbZyy0vPisbH2SyW27+ddLVCN+OMzQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
@vite(['resources/css/app.css', 'resources/js/app.js']) @vite(['resources/css/app.css', 'resources/js/app.js'])
@fluxAppearance @fluxAppearance