diff --git a/PRP.md b/PRP.md index c7bb2db..054ccf8 100644 --- a/PRP.md +++ b/PRP.md @@ -15,9 +15,9 @@ This document outlines the architecture, functionality, and implementation detai ## 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 -* **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:** * Filament 3.3.34 (for the admin panel) * Guzzle 7.9.3 (for HTTP requests) diff --git a/app/Filament/Pages/ListPlugins.php b/app/Filament/Pages/ListPlugins.php deleted file mode 100644 index 02fa6b4..0000000 --- a/app/Filament/Pages/ListPlugins.php +++ /dev/null @@ -1,133 +0,0 @@ -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() -} \ No newline at end of file diff --git a/app/Filament/Pages/Plugin.php b/app/Filament/Pages/Plugin.php new file mode 100644 index 0000000..5939fe6 --- /dev/null +++ b/app/Filament/Pages/Plugin.php @@ -0,0 +1,95 @@ + $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; + } +} \ No newline at end of file diff --git a/app/Filament/Pages/Plugins.php b/app/Filament/Pages/Plugins.php new file mode 100644 index 0000000..45b8133 --- /dev/null +++ b/app/Filament/Pages/Plugins.php @@ -0,0 +1,62 @@ +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 + } +} diff --git a/app/Filament/Resources/PluginResource/CollectionEloquentBuilder.php b/app/Filament/Resources/PluginResource/CollectionEloquentBuilder.php deleted file mode 100644 index 5a2e097..0000000 --- a/app/Filament/Resources/PluginResource/CollectionEloquentBuilder.php +++ /dev/null @@ -1,74 +0,0 @@ -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; - } -} \ No newline at end of file diff --git a/app/Filament/Resources/PluginResource/Plugin.php b/app/Filament/Resources/PluginResource/Plugin.php deleted file mode 100644 index e3d220f..0000000 --- a/app/Filament/Resources/PluginResource/Plugin.php +++ /dev/null @@ -1,36 +0,0 @@ - $value) { - $this->$key = $value; - } - } - - public function newCollection(array $models = []) - { - return new \Illuminate\Database\Eloquent\Collection($models); - } - - public function newEloquentBuilder($query) - { - return new CollectionEloquentBuilder($query); - } -} diff --git a/app/Filament/Resources/SettingResource.php b/app/Filament/Resources/SettingResource.php index ce0f88f..27a508a 100644 --- a/app/Filament/Resources/SettingResource.php +++ b/app/Filament/Resources/SettingResource.php @@ -21,6 +21,8 @@ class SettingResource extends Resource protected static ?string $navigationIcon = 'heroicon-o-cog'; + protected static ?string $navigationGroup = 'Einstellungen'; + public static function form(Form $form): Form { return $form diff --git a/app/Http/Controllers/Admin/PluginController.php b/app/Http/Controllers/Admin/PluginController.php new file mode 100644 index 0000000..3e47f94 --- /dev/null +++ b/app/Http/Controllers/Admin/PluginController.php @@ -0,0 +1,16 @@ +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 + } +} diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php new file mode 100644 index 0000000..bae6983 --- /dev/null +++ b/app/Models/Plugin.php @@ -0,0 +1,66 @@ +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); + } +} \ No newline at end of file diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index ccd5993..33651ec 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -21,6 +21,7 @@ use App\Filament\Resources\StyleResource; use App\Filament\Resources\SettingResource\Pages\Settings; use Illuminate\Support\Facades\Auth; use Illuminate\Support\HtmlString; +use App\Filament\Resources\PluginResource; class AdminPanelProvider extends PanelProvider { @@ -40,13 +41,13 @@ class AdminPanelProvider extends PanelProvider ]) ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\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') ->pages([ Pages\Dashboard::class, \App\Filament\Pages\InstallPluginPage::class, - + // \App\Filament\Pages\ListPlugins::class, // Removed duplicate entry ]) ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->widgets([ @@ -74,7 +75,7 @@ class AdminPanelProvider extends PanelProvider ->profile(); if (Auth::check()) { - $user = Auth->user(); + $user = Auth::user(); if ($user->theme_preference === 'dark') { $panel->darkMode(); } else { diff --git a/resources/views/admin/plugins/index.blade.php b/resources/views/admin/plugins/index.blade.php new file mode 100644 index 0000000..e0a76e0 --- /dev/null +++ b/resources/views/admin/plugins/index.blade.php @@ -0,0 +1,44 @@ +@extends('layouts.app') + +@section('content') +
+

Plugin List

+ +
+ + + + + + + + + + + + @foreach($plugins as $plugin) + + + + + + + + @endforeach + +
NameIdentifierEnabledConfiguredFile Path
{{ $plugin->name }}{{ $plugin->identifier }} + @if($plugin->enabled) + Enabled + @else + Disabled + @endif + + @if($plugin->configured) + Configured + @else + Not Configured + @endif + {{ $plugin->file_path }}
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/filament/pages/list-plugins.blade.php b/resources/views/filament/pages/list-plugins.blade.php deleted file mode 100644 index ce096a2..0000000 --- a/resources/views/filament/pages/list-plugins.blade.php +++ /dev/null @@ -1,3 +0,0 @@ - - {{ $this->table }} - diff --git a/resources/views/filament/pages/plugins.blade.php b/resources/views/filament/pages/plugins.blade.php new file mode 100644 index 0000000..1ed7660 --- /dev/null +++ b/resources/views/filament/pages/plugins.blade.php @@ -0,0 +1,64 @@ + + + + Plugins + + + + + + + Name + + + Identifier + + + Enabled + + + Configured + + + + + @foreach ($this->getTableRecords() as $record) + + + {{ $record->name }} + + + {{ $record->identifier }} + + + @if ($record->enabled) + + @else + + @endif + + + @if ($record->configured) + + @else + + @endif + + + @endforeach + + + + diff --git a/resources/views/livewire/list-plugins.blade.php b/resources/views/livewire/list-plugins.blade.php new file mode 100644 index 0000000..8d83861 --- /dev/null +++ b/resources/views/livewire/list-plugins.blade.php @@ -0,0 +1,5 @@ +
+ + {{ $this->table }} + +
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index a73a052..786e87a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,6 +5,7 @@ use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Route; use Inertia\Inertia; 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::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); 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';