feat: implement DashboardWidgetProvider pattern for modular dashboard widgets
Add protocol-based widget system following the MetricsProvider pattern: - Create DashboardWidgetProviderProtocol in contracts/widgets.py - Add WidgetAggregatorService in core to discover and aggregate widgets - Implement MarketplaceWidgetProvider for recent_imports widget - Implement TenancyWidgetProvider for recent_vendors widget - Update admin dashboard to use widget_aggregator - Add widget_provider field to ModuleDefinition Architecture documentation: - Add widget-provider-pattern.md with implementation guide - Add cross-module-import-rules.md enforcing core/optional separation - Update module-system.md with widget_provider and import rules This enables modules to provide rich dashboard widgets without core modules importing from optional modules, maintaining true module independence. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,17 +5,22 @@ Admin dashboard and statistics endpoints.
|
||||
This module uses the StatsAggregator service from core to collect metrics from all
|
||||
enabled modules. Each module provides its own metrics via the MetricsProvider protocol.
|
||||
|
||||
Dashboard widgets are collected via the WidgetAggregator service, which discovers
|
||||
DashboardWidgetProvider implementations from all enabled modules.
|
||||
|
||||
For backward compatibility, this also falls back to the analytics stats_service
|
||||
for comprehensive statistics that haven't been migrated to the provider pattern yet.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.contracts.widgets import ListWidget
|
||||
from app.modules.core.schemas.dashboard import (
|
||||
AdminDashboardResponse,
|
||||
ImportStatsResponse,
|
||||
@@ -28,7 +33,7 @@ from app.modules.core.schemas.dashboard import (
|
||||
VendorStatsResponse,
|
||||
)
|
||||
from app.modules.core.services.stats_aggregator import stats_aggregator
|
||||
from app.modules.tenancy.services.admin_service import admin_service
|
||||
from app.modules.core.services.widget_aggregator import widget_aggregator
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
admin_dashboard_router = APIRouter(prefix="/dashboard")
|
||||
@@ -77,6 +82,31 @@ def _extract_metric_value(
|
||||
return default
|
||||
|
||||
|
||||
def _widget_list_item_to_dict(item) -> dict[str, Any]:
|
||||
"""Convert a WidgetListItem to a dictionary for API response."""
|
||||
return {
|
||||
"id": item.id,
|
||||
"title": item.title,
|
||||
"subtitle": item.subtitle,
|
||||
"status": item.status,
|
||||
"timestamp": item.timestamp,
|
||||
"url": item.url,
|
||||
**item.metadata,
|
||||
}
|
||||
|
||||
|
||||
def _extract_widget_items(
|
||||
widgets: dict[str, list], category: str, key: str
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Extract items from a list widget for backward compatibility."""
|
||||
if category not in widgets:
|
||||
return []
|
||||
for widget in widgets[category]:
|
||||
if widget.key == key and isinstance(widget.data, ListWidget):
|
||||
return [_widget_list_item_to_dict(item) for item in widget.data.items]
|
||||
return []
|
||||
|
||||
|
||||
@admin_dashboard_router.get("", response_model=AdminDashboardResponse)
|
||||
def get_admin_dashboard(
|
||||
request: Request,
|
||||
@@ -89,6 +119,9 @@ def get_admin_dashboard(
|
||||
# Get aggregated metrics from all enabled modules
|
||||
metrics = stats_aggregator.get_admin_dashboard_stats(db=db, platform_id=platform_id)
|
||||
|
||||
# Get aggregated widgets from all enabled modules
|
||||
widgets = widget_aggregator.get_admin_dashboard_widgets(db=db, platform_id=platform_id)
|
||||
|
||||
# Extract user stats from tenancy module
|
||||
total_users = _extract_metric_value(metrics, "tenancy", "tenancy.total_users", 0)
|
||||
active_users = _extract_metric_value(metrics, "tenancy", "tenancy.active_users", 0)
|
||||
@@ -110,6 +143,12 @@ def get_admin_dashboard(
|
||||
metrics, "tenancy", "tenancy.inactive_vendors", 0
|
||||
)
|
||||
|
||||
# Extract recent_vendors from tenancy widget (backward compatibility)
|
||||
recent_vendors = _extract_widget_items(widgets, "tenancy", "tenancy.recent_vendors")
|
||||
|
||||
# Extract recent_imports from marketplace widget (backward compatibility)
|
||||
recent_imports = _extract_widget_items(widgets, "marketplace", "marketplace.recent_imports")
|
||||
|
||||
return AdminDashboardResponse(
|
||||
platform={
|
||||
"name": "Multi-Tenant Ecommerce Platform",
|
||||
@@ -128,8 +167,8 @@ def get_admin_dashboard(
|
||||
pending=int(pending_vendors),
|
||||
inactive=int(inactive_vendors),
|
||||
),
|
||||
recent_vendors=admin_service.get_recent_vendors(db, limit=5),
|
||||
recent_imports=admin_service.get_recent_import_jobs(db, limit=10),
|
||||
recent_vendors=recent_vendors,
|
||||
recent_imports=recent_imports,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user