132 lines
4.5 KiB
Python
132 lines
4.5 KiB
Python
from __future__ import annotations
|
|
|
|
import inspect
|
|
from collections.abc import Awaitable, Callable
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
from .manager import UIBuilderManager
|
|
|
|
DataProvider = Callable[[], dict[str, Any] | Awaitable[dict[str, Any]]]
|
|
|
|
|
|
class ConfigPayload(BaseModel):
|
|
model_config = ConfigDict(extra="allow")
|
|
|
|
version: int = 20
|
|
project_name: str
|
|
data_source: str
|
|
canvas: dict[str, Any] = Field(default_factory=dict)
|
|
tabs: list[dict[str, Any]] = Field(default_factory=list)
|
|
components: list[dict[str, Any]] = Field(default_factory=list)
|
|
triggers: list[dict[str, Any]] = Field(default_factory=list)
|
|
shortcut_sequences: list[dict[str, Any]] = Field(default_factory=list)
|
|
prematch_groups: list[dict[str, Any]] = Field(default_factory=list)
|
|
prematch_buttons: list[dict[str, Any]] = Field(default_factory=list)
|
|
quick_panel_selectors: list[dict[str, Any]] = Field(default_factory=list)
|
|
|
|
|
|
class BackupPayload(BaseModel):
|
|
name: str
|
|
|
|
|
|
def create_ui_builder_router(
|
|
manager: UIBuilderManager,
|
|
data_providers: dict[str, DataProvider],
|
|
*,
|
|
source_labels: dict[str, str] | None = None,
|
|
prefix: str = "/api/ui-builder",
|
|
access_dependency: Callable[..., Any] | None = None,
|
|
read_only: bool = False,
|
|
publish_manager: UIBuilderManager | None = None,
|
|
) -> APIRouter:
|
|
dependencies = [Depends(access_dependency)] if access_dependency else []
|
|
router = APIRouter(
|
|
prefix=prefix,
|
|
tags=["UI Builder"],
|
|
dependencies=dependencies,
|
|
)
|
|
source_labels = source_labels or {}
|
|
|
|
@router.get("/config")
|
|
async def get_config() -> dict[str, Any]:
|
|
return manager.load()
|
|
|
|
if not read_only:
|
|
@router.post("/config")
|
|
async def save_config(payload: ConfigPayload) -> dict[str, Any]:
|
|
saved = manager.save(payload.model_dump())
|
|
return {"ok": True, "config": saved}
|
|
|
|
@router.post("/import")
|
|
async def import_config(payload: ConfigPayload) -> dict[str, Any]:
|
|
saved = manager.import_config(payload.model_dump())
|
|
return {"ok": True, "config": saved}
|
|
|
|
@router.get("/export")
|
|
async def export_config() -> JSONResponse:
|
|
return JSONResponse(
|
|
manager.load(),
|
|
headers={
|
|
"Content-Disposition":
|
|
'attachment; filename="ui_builder_draft.json"'
|
|
},
|
|
)
|
|
|
|
@router.get("/backups")
|
|
async def list_backups() -> dict[str, Any]:
|
|
return {"items": manager.list_backups()}
|
|
|
|
@router.post("/backups/restore")
|
|
async def restore_backup(payload: BackupPayload) -> dict[str, Any]:
|
|
try:
|
|
restored = manager.restore_backup(payload.name)
|
|
except (FileNotFoundError, ValueError, OSError):
|
|
raise HTTPException(status_code=404, detail="Backup not found")
|
|
return {"ok": True, "config": restored}
|
|
|
|
if publish_manager is not None:
|
|
@router.post("/publish")
|
|
async def publish_config() -> dict[str, Any]:
|
|
published = publish_manager.save(
|
|
manager.load(),
|
|
create_backup=True,
|
|
)
|
|
return {
|
|
"ok": True,
|
|
"config": published,
|
|
"published_at": datetime.now().isoformat(timespec="seconds"),
|
|
}
|
|
|
|
@router.get("/sources")
|
|
async def list_sources() -> dict[str, Any]:
|
|
return {
|
|
"items": [
|
|
{"id": key, "label": source_labels.get(key, key)}
|
|
for key in data_providers
|
|
]
|
|
}
|
|
|
|
@router.get("/data/{source}")
|
|
async def get_data(source: str) -> dict[str, Any]:
|
|
provider = data_providers.get(source)
|
|
if provider is None:
|
|
raise HTTPException(status_code=404, detail="Unknown data source")
|
|
try:
|
|
result = provider()
|
|
if inspect.isawaitable(result):
|
|
result = await result
|
|
if not isinstance(result, dict):
|
|
raise TypeError("Data provider must return a dict")
|
|
return result
|
|
except HTTPException:
|
|
raise
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
|
|
return router
|