Plugins can now register their settings pages dynamically via get_settings_routes()
hook, eliminating hardcoded imports that break lean builds.
Backend:
- Add get_settings_routes() hook to BasePlugin (shopdb/plugins/base.py)
- Implement in ComputersPlugin for Operating Systems management
- Add /api/plugins/settings-routes endpoint to expose plugin routes
Frontend:
- ModelCatalogHub fetches plugin routes and dynamically adds them via router.addRoute()
- Tabs render dynamically from pluginTabs ref
- Components lazy-load when clicked
Benefits:
- Lean builds work without manual route exclusions
- Plugins self-contain their UI extensions
- No core code changes needed when adding plugin settings pages
- Follows plugin architecture best practices
Fixes TODO from commit a03fd3de4f
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
100 lines
3.5 KiB
Python
100 lines
3.5 KiB
Python
"""Plugin introspection + enable/disable API."""
|
|
|
|
from flask import Blueprint, current_app, request
|
|
from flask_jwt_extended import jwt_required
|
|
|
|
from shopdb.utils.responses import success_response, error_response, ErrorCodes
|
|
|
|
from shopdb.utils.authz import require_role
|
|
from shopdb.utils.authz import require_permission
|
|
|
|
plugins_bp = Blueprint('plugins', __name__)
|
|
|
|
|
|
@plugins_bp.route('', methods=['GET'])
|
|
@require_permission('settings.view')
|
|
def list_plugins():
|
|
"""List all discovered plugins (enabled or not) with their metadata and
|
|
the framework contract version."""
|
|
from shopdb import __contract_version__
|
|
pm = current_app.extensions.get('plugin_manager')
|
|
plugins = pm.discover_available() if pm else []
|
|
plugins.sort(key=lambda p: p['name'])
|
|
return success_response({
|
|
'contract_version': __contract_version__,
|
|
'count': len(plugins),
|
|
'plugins': plugins,
|
|
})
|
|
|
|
|
|
@plugins_bp.route('/enabled', methods=['GET'])
|
|
@jwt_required(optional=True)
|
|
def list_enabled_plugins():
|
|
"""Return just the names of enabled plugins as a flat array.
|
|
|
|
Cheap registry read (no DB). Exposed to anonymous callers on purpose:
|
|
the navigation endpoint already leaks the same enabled/disabled signal,
|
|
and the frontend needs it (including unauthenticated kiosk routes like
|
|
/tv) to gate plugin-owned routes. No metadata beyond the names.
|
|
"""
|
|
pm = current_app.extensions.get('plugin_manager')
|
|
names = pm.registry.get_enabled_plugins() if pm else []
|
|
return success_response(sorted(names))
|
|
|
|
|
|
@plugins_bp.route('/settings-routes', methods=['GET'])
|
|
@jwt_required(optional=True)
|
|
def get_plugin_settings_routes():
|
|
"""Return settings routes contributed by all enabled plugins.
|
|
|
|
Plugins can register routes to add tabs/pages to settings hubs like
|
|
Model Catalog, Asset Config, etc. Frontend router merges these dynamically.
|
|
|
|
Returns: { routes: [{hub, path, name, tab, component}, ...] }
|
|
"""
|
|
pm = current_app.extensions.get('plugin_manager')
|
|
if not pm:
|
|
return success_response({'routes': []})
|
|
|
|
routes = []
|
|
for plugin in pm.get_enabled_plugin_instances():
|
|
if hasattr(plugin, 'get_settings_routes'):
|
|
plugin_routes = plugin.get_settings_routes()
|
|
if plugin_routes:
|
|
routes.extend(plugin_routes)
|
|
|
|
return success_response({'routes': routes})
|
|
|
|
|
|
@plugins_bp.route('/<name>', methods=['PUT'])
|
|
@jwt_required()
|
|
@require_role('admin')
|
|
def set_plugin_enabled(name: str):
|
|
"""Enable or disable a plugin. Takes effect on the next app restart for
|
|
route/navigation changes."""
|
|
data = request.get_json() or {}
|
|
if 'enabled' not in data:
|
|
return error_response(ErrorCodes.VALIDATION_ERROR, 'enabled is required')
|
|
|
|
pm = current_app.extensions.get('plugin_manager')
|
|
if not pm:
|
|
return error_response(ErrorCodes.INTERNAL_ERROR,
|
|
'Plugin manager unavailable', http_code=500)
|
|
|
|
want = bool(data['enabled'])
|
|
ok = pm.enable_plugin(name) if want else pm.disable_plugin(name)
|
|
if not ok:
|
|
# enable/disable refused (unknown plugin, or a dependency conflict)
|
|
return error_response(
|
|
ErrorCodes.CONFLICT,
|
|
f"Could not {'enable' if want else 'disable'} '{name}' "
|
|
f"(check it exists and dependencies allow it)",
|
|
http_code=409
|
|
)
|
|
|
|
return success_response(
|
|
{'name': name, 'enabled': want},
|
|
message=f"Plugin {'enabled' if want else 'disabled'} "
|
|
f"(restart to apply route changes)"
|
|
)
|