plugin list works now finally
This commit is contained in:
4
PRP.md
4
PRP.md
@@ -15,9 +15,9 @@ This document outlines the architecture, functionality, and implementation detai
|
|||||||
|
|
||||||
## 2. Core Technologies & Stack
|
## 2. Core Technologies & Stack
|
||||||
|
|
||||||
* **Languages:** PHP 8.1, JavaScript
|
* **Languages:** PHP 8.3, JavaScript
|
||||||
* **Frameworks & Runtimes:** Laravel 12.21.0, Vue.js 3.5.18, Inertia.js 1.3.0, Livewire 3.6.4, Vite 5.4.19
|
* **Frameworks & Runtimes:** Laravel 12.21.0, Vue.js 3.5.18, Inertia.js 1.3.0, Livewire 3.6.4, Vite 5.4.19
|
||||||
* **Databases:** MySQL (default), with configurations for PostgreSQL, SQLite, and SQL Server. Redis is used for caching.
|
* **Databases:** SQLite. Redis is used for caching.
|
||||||
* **Key PHP Libraries/Dependencies:**
|
* **Key PHP Libraries/Dependencies:**
|
||||||
* Filament 3.3.34 (for the admin panel)
|
* Filament 3.3.34 (for the admin panel)
|
||||||
* Guzzle 7.9.3 (for HTTP requests)
|
* Guzzle 7.9.3 (for HTTP requests)
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Pages;
|
|
||||||
|
|
||||||
use App\Api\Plugins\ApiPluginInterface;
|
|
||||||
use App\Filament\Resources\PluginResource\Plugin;
|
|
||||||
use Filament\Notifications\Notification;
|
|
||||||
use Filament\Pages\Page;
|
|
||||||
use Filament\Tables\Columns\IconColumn;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Concerns\InteractsWithTable;
|
|
||||||
use Filament\Tables\Contracts\HasTable;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
|
||||||
use Illuminate\Support\Facades\File;
|
|
||||||
use App\Models\ApiProvider;
|
|
||||||
use Filament\Tables\Actions\Action;
|
|
||||||
|
|
||||||
class ListPlugins extends Page implements HasTable
|
|
||||||
{
|
|
||||||
use InteractsWithTable;
|
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-puzzle-piece';
|
|
||||||
|
|
||||||
protected static string $view = 'filament.pages.list-plugins';
|
|
||||||
|
|
||||||
protected static ?string $navigationGroup = 'Plugins';
|
|
||||||
|
|
||||||
protected static ?string $title = 'Plugins';
|
|
||||||
|
|
||||||
public function table(Table $table): Table
|
|
||||||
{
|
|
||||||
$plugins = new Collection();
|
|
||||||
$path = app_path('Api/Plugins');
|
|
||||||
|
|
||||||
if (File::exists($path)) {
|
|
||||||
$files = File::files($path);
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$filename = $file->getFilenameWithoutExtension();
|
|
||||||
$class = 'App\\Api\\Plugins\\' . $filename;
|
|
||||||
|
|
||||||
if (class_exists($class) && in_array(ApiPluginInterface::class, class_implements($class))) {
|
|
||||||
try {
|
|
||||||
$apiProvider = ApiProvider::where('plugin', $filename)->first();
|
|
||||||
if(!$apiProvider) continue;
|
|
||||||
$instance = new $class($apiProvider);
|
|
||||||
$plugins->add(new Plugin([
|
|
||||||
'id' => $instance->getIdentifier(),
|
|
||||||
'name' => $instance->getName(),
|
|
||||||
'identifier' => $instance->getIdentifier(),
|
|
||||||
'enabled' => $instance->isEnabled(),
|
|
||||||
'file_path' => $file->getPathname(),
|
|
||||||
]));
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
// Log error or handle gracefully if a plugin cannot be instantiated
|
|
||||||
// For now, we'll just skip it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $table
|
|
||||||
->query(Plugin::query()->setCollection($plugins))
|
|
||||||
->columns([
|
|
||||||
TextColumn::make('name')
|
|
||||||
->label('Name')
|
|
||||||
->searchable()
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('identifier')
|
|
||||||
->label('Identifier'),
|
|
||||||
IconColumn::make('enabled')
|
|
||||||
->label('Enabled')
|
|
||||||
->boolean(),
|
|
||||||
TextColumn::make('file_path')
|
|
||||||
->label('File Path')
|
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
|
||||||
])
|
|
||||||
->actions([
|
|
||||||
Action::make('toggle_enabled')
|
|
||||||
->label(fn ($record) => $record->enabled ? 'Disable' : 'Enable')
|
|
||||||
->icon(fn ($record) => $record->enabled ? 'heroicon-o-x-circle' : 'heroicon-o-check-circle')
|
|
||||||
->action(function ($record) {
|
|
||||||
try {
|
|
||||||
$apiProvider = ApiProvider::where('plugin', $record->identifier)->first();
|
|
||||||
if(!$apiProvider) throw new \Exception('ApiProvider not found');
|
|
||||||
$pluginClass = 'App\\Api\\Plugins\\' . $record->identifier;
|
|
||||||
$plugin = new $pluginClass($apiProvider);
|
|
||||||
if ($record->enabled) {
|
|
||||||
$plugin->disable();
|
|
||||||
} else {
|
|
||||||
$plugin->enable();
|
|
||||||
}
|
|
||||||
Notification::make()
|
|
||||||
->title('Plugin status updated')
|
|
||||||
->success()
|
|
||||||
->send();
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Notification::make()
|
|
||||||
->title('Error updating plugin status')
|
|
||||||
->body($e->getMessage())
|
|
||||||
->danger()
|
|
||||||
->send();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Action::make('delete')
|
|
||||||
->label('Delete')
|
|
||||||
->icon('heroicon-o-trash')
|
|
||||||
->color('danger')
|
|
||||||
->requiresConfirmation()
|
|
||||||
->action(function ($record) {
|
|
||||||
try {
|
|
||||||
File::delete($record->file_path);
|
|
||||||
Notification::make()
|
|
||||||
->title('Plugin deleted successfully')
|
|
||||||
->success()
|
|
||||||
->send();
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Notification::make()
|
|
||||||
->title('Error deleting plugin')
|
|
||||||
->body($e->getMessage())
|
|
||||||
->danger()
|
|
||||||
->send();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
->bulkActions([
|
|
||||||
// No bulk actions for now
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removed getTableRecords() as data is now provided via query()
|
|
||||||
}
|
|
||||||
95
app/Filament/Pages/Plugin.php
Normal file
95
app/Filament/Pages/Plugin.php
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Api\Plugins\ApiPluginInterface;
|
||||||
|
use App\Models\ApiProvider;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Exception;
|
||||||
|
use ReflectionClass;
|
||||||
|
|
||||||
|
class Plugin extends Model
|
||||||
|
{
|
||||||
|
protected $table = null; // No actual table
|
||||||
|
|
||||||
|
protected $guarded = []; // Allow mass assignment for all attributes
|
||||||
|
|
||||||
|
public $incrementing = false;
|
||||||
|
|
||||||
|
protected $keyType = 'string';
|
||||||
|
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
public function __construct(array $attributes = [])
|
||||||
|
{
|
||||||
|
parent::__construct($attributes);
|
||||||
|
foreach ($attributes as $key => $value) {
|
||||||
|
$this->$key = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getAllPlugins()
|
||||||
|
{
|
||||||
|
$plugins = [];
|
||||||
|
$path = app_path('Api/Plugins');
|
||||||
|
|
||||||
|
if (File::exists($path)) {
|
||||||
|
$files = File::files($path);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$filename = $file->getFilenameWithoutExtension();
|
||||||
|
if (in_array($filename, ['ApiPluginInterface', 'LoggablePlugin', 'PluginLoader'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$class = 'App\Api\Plugins\\' . $filename;
|
||||||
|
|
||||||
|
if (class_exists($class) && in_array(ApiPluginInterface::class, class_implements($class))) {
|
||||||
|
try {
|
||||||
|
// Check if there's an ApiProvider for this plugin
|
||||||
|
$apiProvider = ApiProvider::where('plugin', $filename)->first();
|
||||||
|
$hasApiProvider = $apiProvider !== null;
|
||||||
|
|
||||||
|
// Get plugin information without instantiating the class
|
||||||
|
// This avoids issues with plugins requiring ApiProvider in constructor
|
||||||
|
$reflection = new ReflectionClass($class);
|
||||||
|
$instance = $reflection->newInstanceWithoutConstructor();
|
||||||
|
|
||||||
|
$plugins[] = new self([
|
||||||
|
'id' => $instance->getIdentifier(),
|
||||||
|
'name' => $instance->getName(),
|
||||||
|
'identifier' => $instance->getIdentifier(),
|
||||||
|
'enabled' => $hasApiProvider && $apiProvider->enabled,
|
||||||
|
'file_path' => $file->getPathname(),
|
||||||
|
'has_api_provider' => $hasApiProvider,
|
||||||
|
'configured' => $hasApiProvider
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Log error or handle as needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newCollection(array $models = [])
|
||||||
|
{
|
||||||
|
return new \Illuminate\Database\Eloquent\Collection($models);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newQuery()
|
||||||
|
{
|
||||||
|
// Create a new query builder instance
|
||||||
|
$query = new \Illuminate\Database\Eloquent\Builder(
|
||||||
|
new \Illuminate\Database\Query\Builder(
|
||||||
|
\Illuminate\Support\Facades\DB::connection()->getQueryGrammar(),
|
||||||
|
\Illuminate\Support\Facades\DB::connection()->getPostProcessor()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set the model for the query builder
|
||||||
|
$query->setModel($this);
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/Filament/Pages/Plugins.php
Normal file
62
app/Filament/Pages/Plugins.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Filament\Forms\Contracts\HasForms;
|
||||||
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
|
use App\Models\Plugin;
|
||||||
|
|
||||||
|
class Plugins extends Page implements Tables\Contracts\HasTable, HasForms
|
||||||
|
{
|
||||||
|
use Tables\Concerns\InteractsWithTable;
|
||||||
|
use InteractsWithForms;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'heroicon-o-puzzle-piece';
|
||||||
|
|
||||||
|
protected static ?string $navigationGroup = 'Plugins';
|
||||||
|
|
||||||
|
protected static ?string $title = 'Plugins';
|
||||||
|
|
||||||
|
protected static string $view = 'filament.pages.plugins';
|
||||||
|
|
||||||
|
protected static ?string $slug = 'list-plugins';
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->query(Plugin::query()) // Use a dummy query
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('name')
|
||||||
|
->label('Name')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('identifier')
|
||||||
|
->label('Identifier'),
|
||||||
|
Tables\Columns\IconColumn::make('enabled')
|
||||||
|
->label('Enabled')
|
||||||
|
->boolean(),
|
||||||
|
Tables\Columns\IconColumn::make('configured')
|
||||||
|
->label('Configured')
|
||||||
|
->boolean()
|
||||||
|
->tooltip(fn ($record) => $record->configured ? 'Has ApiProvider record' : 'No ApiProvider record'),
|
||||||
|
Tables\Columns\TextColumn::make('file_path')
|
||||||
|
->label('File Path')
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTableRecords(): \Illuminate\Database\Eloquent\Collection
|
||||||
|
{
|
||||||
|
// Get all plugins as a collection
|
||||||
|
return Plugin::getAllPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function currentlyValidatingForm(\Filament\Forms\ComponentContainer|null $form): void
|
||||||
|
{
|
||||||
|
// No form validation needed for this page
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\PluginResource;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
|
||||||
use Illuminate\Pagination\Paginator;
|
|
||||||
|
|
||||||
class CollectionEloquentBuilder extends Builder
|
|
||||||
{
|
|
||||||
protected $collection;
|
|
||||||
|
|
||||||
public function __construct($query)
|
|
||||||
{
|
|
||||||
parent::__construct($query);
|
|
||||||
$this->collection = new Collection(); // Initialize with an empty collection
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setCollection(Collection $collection)
|
|
||||||
{
|
|
||||||
$this->collection = $collection;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get($columns = ['*'])
|
|
||||||
{
|
|
||||||
return $this->collection;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function find($id, $columns = ['*'])
|
|
||||||
{
|
|
||||||
return $this->collection->firstWhere('id', $id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null, $total = null)
|
|
||||||
{
|
|
||||||
$page = $page ?: Paginator::resolveCurrentPage($pageName);
|
|
||||||
|
|
||||||
$results = $this->collection->slice(($page - 1) * $perPage, $perPage)->all();
|
|
||||||
|
|
||||||
return new LengthAwarePaginator($results, $this->collection->count(), $perPage, $page, [
|
|
||||||
'path' => Paginator::resolveCurrentPath(),
|
|
||||||
'pageName' => $pageName,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function count($columns = '*')
|
|
||||||
{
|
|
||||||
return $this->collection->count();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function where($column, $operator = null, $value = null, $boolean = 'and')
|
|
||||||
{
|
|
||||||
if (func_num_args() === 2) {
|
|
||||||
[$value, $operator] = [$operator, '='];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($operator === '=') {
|
|
||||||
$this->collection = $this->collection->where($column, $value);
|
|
||||||
} else {
|
|
||||||
// For simplicity, only handling '=' operator for now. More complex operators would require more logic.
|
|
||||||
// For example, for 'like', you'd need to implement string matching.
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function orderBy($column, $direction = 'asc')
|
|
||||||
{
|
|
||||||
$this->collection = $this->collection->sortBy($column, SORT_REGULAR, $direction === 'desc');
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\PluginResource;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class Plugin extends Model
|
|
||||||
{
|
|
||||||
protected $table = null; // No actual table
|
|
||||||
|
|
||||||
protected $guarded = []; // Allow mass assignment for all attributes
|
|
||||||
|
|
||||||
public $incrementing = false;
|
|
||||||
|
|
||||||
protected $keyType = 'string';
|
|
||||||
|
|
||||||
protected $primaryKey = 'id';
|
|
||||||
|
|
||||||
public function __construct(array $attributes = [])
|
|
||||||
{
|
|
||||||
parent::__construct($attributes);
|
|
||||||
foreach ($attributes as $key => $value) {
|
|
||||||
$this->$key = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function newCollection(array $models = [])
|
|
||||||
{
|
|
||||||
return new \Illuminate\Database\Eloquent\Collection($models);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function newEloquentBuilder($query)
|
|
||||||
{
|
|
||||||
return new CollectionEloquentBuilder($query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,6 +21,8 @@ class SettingResource extends Resource
|
|||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-cog';
|
protected static ?string $navigationIcon = 'heroicon-o-cog';
|
||||||
|
|
||||||
|
protected static ?string $navigationGroup = 'Einstellungen';
|
||||||
|
|
||||||
public static function form(Form $form): Form
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $form
|
return $form
|
||||||
|
|||||||
16
app/Http/Controllers/Admin/PluginController.php
Normal file
16
app/Http/Controllers/Admin/PluginController.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Plugin;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class PluginController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$plugins = Plugin::getAllPlugins();
|
||||||
|
return view('admin.plugins.index', compact('plugins'));
|
||||||
|
}
|
||||||
|
}
|
||||||
121
app/Livewire/ListPlugins.php
Normal file
121
app/Livewire/ListPlugins.php
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Api\Plugins\ApiPluginInterface;
|
||||||
|
use App\Models\Plugin;
|
||||||
|
use App\Models\ApiProvider;
|
||||||
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
|
use Filament\Forms\Contracts\HasForms;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Tables\Actions\Action;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Concerns\InteractsWithTable;
|
||||||
|
use Filament\Tables\Contracts\HasTable;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class ListPlugins extends Component implements HasForms, HasTable
|
||||||
|
{
|
||||||
|
use InteractsWithForms;
|
||||||
|
use InteractsWithTable;
|
||||||
|
|
||||||
|
public function getPlugins(): Collection
|
||||||
|
{
|
||||||
|
$plugins = new Collection();
|
||||||
|
$path = app_path('Api/Plugins');
|
||||||
|
|
||||||
|
if (File::exists($path)) {
|
||||||
|
$files = File::files($path);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$filename = $file->getFilenameWithoutExtension();
|
||||||
|
if (in_array($filename, ['ApiPluginInterface', 'LoggablePlugin', 'PluginLoader'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$class = 'App\Api\Plugins\\' . $filename;
|
||||||
|
|
||||||
|
if (class_exists($class) && in_array(ApiPluginInterface::class, class_implements($class))) {
|
||||||
|
try {
|
||||||
|
// Check if there's an ApiProvider for this plugin
|
||||||
|
$apiProvider = ApiProvider::where('plugin', $filename)->first();
|
||||||
|
$hasApiProvider = $apiProvider !== null;
|
||||||
|
|
||||||
|
// Get plugin information without instantiating the class
|
||||||
|
// This avoids issues with plugins requiring ApiProvider in constructor
|
||||||
|
$reflection = new \ReflectionClass($class);
|
||||||
|
$instance = $reflection->newInstanceWithoutConstructor();
|
||||||
|
|
||||||
|
$plugins->add(new Plugin([
|
||||||
|
'id' => $instance->getIdentifier(),
|
||||||
|
'name' => $instance->getName(),
|
||||||
|
'identifier' => $instance->getIdentifier(),
|
||||||
|
'enabled' => $hasApiProvider && $apiProvider->enabled,
|
||||||
|
'file_path' => $file->getPathname(),
|
||||||
|
'has_api_provider' => $hasApiProvider,
|
||||||
|
'configured' => $hasApiProvider
|
||||||
|
]));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Error loading plugin for ListPlugins page.', ['plugin_file' => $file->getPathname(), 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->query(fn () => $this->getPlugins())
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('name')
|
||||||
|
->label('Name')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
TextColumn::make('identifier')
|
||||||
|
->label('Identifier'),
|
||||||
|
IconColumn::make('enabled')
|
||||||
|
->label('Enabled')
|
||||||
|
->boolean(),
|
||||||
|
IconColumn::make('configured')
|
||||||
|
->label('Configured')
|
||||||
|
->boolean()
|
||||||
|
->tooltip(fn ($record) => $record->configured ? 'Has ApiProvider record' : 'No ApiProvider record'),
|
||||||
|
TextColumn::make('file_path')
|
||||||
|
->label('File Path')
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Action::make('toggle_enabled')
|
||||||
|
->label(fn($record) => $record->enabled ? 'Disable' : 'Enable')
|
||||||
|
->icon(fn($record) => $record->enabled ? 'heroicon-o-x-circle' :
|
||||||
|
'heroicon-o-check-circle')
|
||||||
|
->action(function ($record) {
|
||||||
|
// Action logic here
|
||||||
|
}),
|
||||||
|
Action::make('delete')
|
||||||
|
->label('Delete')
|
||||||
|
->icon('heroicon-o-trash')
|
||||||
|
->color('danger')
|
||||||
|
->requiresConfirmation()
|
||||||
|
->action(function ($record) {
|
||||||
|
// Action logic here
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.list-plugins');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function currentlyValidatingForm(\Filament\Forms\ComponentContainer|null $form): void
|
||||||
|
{
|
||||||
|
// No form validation needed for this component
|
||||||
|
}
|
||||||
|
}
|
||||||
66
app/Models/Plugin.php
Normal file
66
app/Models/Plugin.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Api\Plugins\ApiPluginInterface;
|
||||||
|
use App\Models\ApiProvider;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Exception;
|
||||||
|
use ReflectionClass;
|
||||||
|
|
||||||
|
class Plugin extends Model
|
||||||
|
{
|
||||||
|
protected $table = null; // No actual table
|
||||||
|
|
||||||
|
protected $guarded = []; // Allow mass assignment for all attributes
|
||||||
|
|
||||||
|
public $incrementing = false;
|
||||||
|
|
||||||
|
protected $keyType = 'string';
|
||||||
|
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
public static function getAllPlugins()
|
||||||
|
{
|
||||||
|
$plugins = [];
|
||||||
|
$path = app_path('Api/Plugins');
|
||||||
|
|
||||||
|
if (File::exists($path)) {
|
||||||
|
$files = File::files($path);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$filename = $file->getFilenameWithoutExtension();
|
||||||
|
if (in_array($filename, ['ApiPluginInterface', 'LoggablePlugin', 'PluginLoader'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$class = 'App\Api\Plugins\\' . $filename;
|
||||||
|
|
||||||
|
if (class_exists($class) && in_array(ApiPluginInterface::class, class_implements($class))) {
|
||||||
|
try {
|
||||||
|
// Check if there's an ApiProvider for this plugin
|
||||||
|
$apiProvider = ApiProvider::where('plugin', $filename)->first();
|
||||||
|
$hasApiProvider = $apiProvider !== null;
|
||||||
|
|
||||||
|
// Get plugin information without instantiating the class
|
||||||
|
// This avoids issues with plugins requiring ApiProvider in constructor
|
||||||
|
$reflection = new ReflectionClass($class);
|
||||||
|
$instance = $reflection->newInstanceWithoutConstructor();
|
||||||
|
|
||||||
|
$plugins[] = new self([
|
||||||
|
'id' => $instance->getIdentifier(),
|
||||||
|
'name' => $instance->getName(),
|
||||||
|
'identifier' => $instance->getIdentifier(),
|
||||||
|
'enabled' => $hasApiProvider && $apiProvider && $apiProvider->enabled,
|
||||||
|
'file_path' => $file->getPathname(),
|
||||||
|
'has_api_provider' => $hasApiProvider,
|
||||||
|
'configured' => $hasApiProvider
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Log error or handle as needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new \Illuminate\Database\Eloquent\Collection($plugins);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ use App\Filament\Resources\StyleResource;
|
|||||||
use App\Filament\Resources\SettingResource\Pages\Settings;
|
use App\Filament\Resources\SettingResource\Pages\Settings;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\HtmlString;
|
use Illuminate\Support\HtmlString;
|
||||||
|
use App\Filament\Resources\PluginResource;
|
||||||
|
|
||||||
class AdminPanelProvider extends PanelProvider
|
class AdminPanelProvider extends PanelProvider
|
||||||
{
|
{
|
||||||
@@ -40,13 +41,13 @@ class AdminPanelProvider extends PanelProvider
|
|||||||
])
|
])
|
||||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
|
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
|
||||||
->resources([
|
->resources([
|
||||||
\App\Filament\Resources\ApiProviderResource::class,
|
// PluginResource::class, // Removed as it's a custom page now
|
||||||
])
|
])
|
||||||
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
|
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
|
||||||
->pages([
|
->pages([
|
||||||
Pages\Dashboard::class,
|
Pages\Dashboard::class,
|
||||||
\App\Filament\Pages\InstallPluginPage::class,
|
\App\Filament\Pages\InstallPluginPage::class,
|
||||||
|
// \App\Filament\Pages\ListPlugins::class, // Removed duplicate entry
|
||||||
])
|
])
|
||||||
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
|
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
|
||||||
->widgets([
|
->widgets([
|
||||||
@@ -74,7 +75,7 @@ class AdminPanelProvider extends PanelProvider
|
|||||||
->profile();
|
->profile();
|
||||||
|
|
||||||
if (Auth::check()) {
|
if (Auth::check()) {
|
||||||
$user = Auth->user();
|
$user = Auth::user();
|
||||||
if ($user->theme_preference === 'dark') {
|
if ($user->theme_preference === 'dark') {
|
||||||
$panel->darkMode();
|
$panel->darkMode();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
44
resources/views/admin/plugins/index.blade.php
Normal file
44
resources/views/admin/plugins/index.blade.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-6">Plugin List</h1>
|
||||||
|
|
||||||
|
<div class="bg-white shadow-md rounded-lg overflow-hidden">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Identifier</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Enabled</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Configured</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">File Path</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
@foreach($plugins as $plugin)
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ $plugin->name }}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $plugin->identifier }}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
@if($plugin->enabled)
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Enabled</span>
|
||||||
|
@else
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Disabled</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
@if($plugin->configured)
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Configured</span>
|
||||||
|
@else
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">Not Configured</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-500">{{ $plugin->file_path }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<x-filament-panels::page>
|
|
||||||
{{ $this->table }}
|
|
||||||
</x-filament-panels::page>
|
|
||||||
64
resources/views/filament/pages/plugins.blade.php
Normal file
64
resources/views/filament/pages/plugins.blade.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<x-filament-panels::page>
|
||||||
|
<x-filament::section>
|
||||||
|
<x-slot name="heading">
|
||||||
|
Plugins
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<x-filament-tables::table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<x-filament-tables::header-cell>
|
||||||
|
Name
|
||||||
|
</x-filament-tables::header-cell>
|
||||||
|
<x-filament-tables::header-cell>
|
||||||
|
Identifier
|
||||||
|
</x-filament-tables::header-cell>
|
||||||
|
<x-filament-tables::header-cell>
|
||||||
|
Enabled
|
||||||
|
</x-filament-tables::header-cell>
|
||||||
|
<x-filament-tables::header-cell>
|
||||||
|
Configured
|
||||||
|
</x-filament-tables::header-cell>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach ($this->getTableRecords() as $record)
|
||||||
|
<x-filament-tables::row>
|
||||||
|
<x-filament-tables::cell>
|
||||||
|
{{ $record->name }}
|
||||||
|
</x-filament-tables::cell>
|
||||||
|
<x-filament-tables::cell>
|
||||||
|
{{ $record->identifier }}
|
||||||
|
</x-filament-tables::cell>
|
||||||
|
<x-filament-tables::cell>
|
||||||
|
@if ($record->enabled)
|
||||||
|
<x-filament::icon
|
||||||
|
icon="heroicon-o-check-circle"
|
||||||
|
class="text-success-500"
|
||||||
|
/>
|
||||||
|
@else
|
||||||
|
<x-filament::icon
|
||||||
|
icon="heroicon-o-x-circle"
|
||||||
|
class="text-danger-500"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
</x-filament-tables::cell>
|
||||||
|
<x-filament-tables::cell>
|
||||||
|
@if ($record->configured)
|
||||||
|
<x-filament::icon
|
||||||
|
icon="heroicon-o-check-circle"
|
||||||
|
class="text-success-500"
|
||||||
|
/>
|
||||||
|
@else
|
||||||
|
<x-filament::icon
|
||||||
|
icon="heroicon-o-x-circle"
|
||||||
|
class="text-danger-500"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
</x-filament-tables::cell>
|
||||||
|
</x-filament-tables::row>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</x-filament-tables::table>
|
||||||
|
</x-filament::section>
|
||||||
|
</x-filament-panels::page>
|
||||||
5
resources/views/livewire/list-plugins.blade.php
Normal file
5
resources/views/livewire/list-plugins.blade.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<div>
|
||||||
|
<x-filament-panels::page>
|
||||||
|
{{ $this->table }}
|
||||||
|
</x-filament-panels::page>
|
||||||
|
</div>
|
||||||
@@ -5,6 +5,7 @@ use Illuminate\Foundation\Application;
|
|||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use App\Http\Controllers\LocaleController;
|
use App\Http\Controllers\LocaleController;
|
||||||
|
use App\Http\Controllers\Admin\PluginController;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -27,6 +28,9 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||||
|
|
||||||
|
// Plugin routes
|
||||||
|
Route::get('/admin/plugins', [PluginController::class, 'index'])->name('admin.plugins.index');
|
||||||
});
|
});
|
||||||
|
|
||||||
require __DIR__.'/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
|
|||||||
Reference in New Issue
Block a user