feat(maintenance): add dashboard preview and expandable event list
- Add POST /api/maintenance/preview-dboards endpoint for table-to-dashboard preview - Extend GET /api/maintenance/events with per-event dashboard list (id+title) - Add 'Show affected dashboards' button to StartMaintenanceForm - Add expandable row to MaintenanceEventsTable (click count to see names) - Resolve dashboard titles via SupersetClient.get_dashboards() lookup map - Add i18n keys for preview feature (en/ru)
This commit is contained in:
@@ -40,6 +40,9 @@ from ._router import router
|
||||
from ._schemas import (
|
||||
MaintenanceAlreadyActiveResponse,
|
||||
MaintenanceDashboardBannerState,
|
||||
MaintenanceDashboardInfo,
|
||||
MaintenanceDashboardPreviewRequest,
|
||||
MaintenanceDashboardPreviewResponse,
|
||||
MaintenanceEndResponse,
|
||||
MaintenanceEventItem,
|
||||
MaintenanceEventListResponse,
|
||||
@@ -119,7 +122,7 @@ async def list_dashboard_banners(
|
||||
|
||||
# #region list_events [C:2] [TYPE Function]
|
||||
# @ingroup Api
|
||||
# @BRIEF Get active and completed maintenance event lists with affected dashboard counts.
|
||||
# @BRIEF Get active and completed maintenance event lists with affected dashboard counts and names.
|
||||
# @RELATION DEPENDS_ON -> [EXT:code:has_permission_maintenance_READ]
|
||||
@router.get("/events", response_model=MaintenanceEventListResponse)
|
||||
async def list_events(
|
||||
@@ -189,8 +192,39 @@ async def list_events(
|
||||
.all()
|
||||
)
|
||||
|
||||
# ── Build dashboard title lookup from Superset ──
|
||||
dashboard_title_map: dict[int, str] = {}
|
||||
try:
|
||||
from ....dependencies import get_config_manager as _gcm
|
||||
config_manager = _gcm()
|
||||
# Collect unique environment IDs from all events
|
||||
env_ids = {e.environment_id for e in active_events + completed_events if e.environment_id}
|
||||
for env_id in env_ids:
|
||||
env = next((e for e in config_manager.get_environments() if e.id == env_id), None)
|
||||
if env:
|
||||
try:
|
||||
from ....core.superset_client import SupersetClient
|
||||
client = SupersetClient(env)
|
||||
_, dashboards = await client.get_dashboards()
|
||||
for d in dashboards:
|
||||
did = d.get("id")
|
||||
title = d.get("dashboard_title") or d.get("title") or str(did)
|
||||
if did is not None:
|
||||
dashboard_title_map[int(did)] = title
|
||||
except Exception as e:
|
||||
app_logger.explore(
|
||||
"Failed to fetch dashboards for title lookup",
|
||||
extra={"env_id": env_id},
|
||||
error=str(e),
|
||||
)
|
||||
except Exception as e:
|
||||
app_logger.explore(
|
||||
"Failed to build dashboard title map",
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
def _to_item(event: MaintenanceEvent) -> MaintenanceEventItem:
|
||||
affected_count = (
|
||||
states = (
|
||||
db.query(MaintenanceDashboardState)
|
||||
.filter(
|
||||
MaintenanceDashboardState.event_id == event.id,
|
||||
@@ -199,8 +233,16 @@ async def list_events(
|
||||
MaintenanceDashboardStateStatus.PENDING_APPLY,
|
||||
]),
|
||||
)
|
||||
.count()
|
||||
.all()
|
||||
)
|
||||
affected_count = len(states)
|
||||
dashboards = [
|
||||
MaintenanceDashboardInfo(
|
||||
id=state.dashboard_id,
|
||||
title=dashboard_title_map.get(state.dashboard_id, str(state.dashboard_id)),
|
||||
)
|
||||
for state in states
|
||||
]
|
||||
return MaintenanceEventItem(
|
||||
id=event.id,
|
||||
tables=list(event.tables) if event.tables else [],
|
||||
@@ -209,6 +251,7 @@ async def list_events(
|
||||
message=event.message or None,
|
||||
status=event.status.value,
|
||||
affected_count=affected_count,
|
||||
dashboards=dashboards,
|
||||
created_at=event.created_at.isoformat() if event.created_at else "",
|
||||
)
|
||||
|
||||
@@ -219,6 +262,88 @@ async def list_events(
|
||||
# #endregion list_events
|
||||
|
||||
|
||||
# ── POST /api/maintenance/preview-dashboards ────────────────
|
||||
# FR-015: operator, admin (NOT viewer)
|
||||
|
||||
# #region preview_dashboards [C:3] [TYPE Function]
|
||||
# @ingroup Api
|
||||
# @BRIEF Preview which dashboards would be affected by a maintenance event for given tables.
|
||||
# Calls find_affected_dashboards then resolves titles from Superset.
|
||||
# @RELATION DEPENDS_ON -> [EXT:code:has_permission_maintenance_READ]
|
||||
# @RELATION DEPENDS_ON -> [find_affected_dashboards]
|
||||
@router.post("/preview-dashboards", response_model=MaintenanceDashboardPreviewResponse)
|
||||
async def preview_dashboards(
|
||||
request: MaintenanceDashboardPreviewRequest,
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(has_permission("maintenance", "READ")),
|
||||
):
|
||||
with belief_scope("preview_dashboards"):
|
||||
# Resolve environment
|
||||
from ....dependencies import get_config_manager as _gcm
|
||||
config_manager = _gcm()
|
||||
env = next(
|
||||
(e for e in config_manager.get_environments() if e.id == request.environment_id),
|
||||
None,
|
||||
)
|
||||
if not env:
|
||||
raise HTTPException(status_code=404, detail=f"Environment {request.environment_id} not found")
|
||||
|
||||
from ....core.superset_client import SupersetClient
|
||||
from ....services.maintenance._dashboard_scanner import find_affected_dashboards
|
||||
|
||||
client = SupersetClient(env)
|
||||
|
||||
# Load settings for scope/excluded/forced filtering
|
||||
settings = db.query(MaintenanceSettings).filter(
|
||||
MaintenanceSettings.id == "default"
|
||||
).first()
|
||||
|
||||
# Find affected dashboard IDs
|
||||
try:
|
||||
dashboard_ids = await find_affected_dashboards(
|
||||
request.tables,
|
||||
client,
|
||||
settings,
|
||||
)
|
||||
except Exception as e:
|
||||
app_logger.explore(
|
||||
"Preview failed",
|
||||
extra={"tables": request.tables},
|
||||
error=str(e),
|
||||
)
|
||||
raise HTTPException(status_code=502, detail=f"Failed to query Superset: {e}")
|
||||
|
||||
# Build title lookup
|
||||
title_map: dict[int, str] = {}
|
||||
try:
|
||||
_, all_dashboards = await client.get_dashboards()
|
||||
for d in all_dashboards:
|
||||
did = d.get("id")
|
||||
title = d.get("dashboard_title") or d.get("title") or str(did)
|
||||
if did is not None:
|
||||
title_map[int(did)] = title
|
||||
except Exception as e:
|
||||
app_logger.explore(
|
||||
"Failed to fetch dashboards for title resolution",
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
result = [
|
||||
MaintenanceDashboardInfo(
|
||||
id=did,
|
||||
title=title_map.get(did, str(did)),
|
||||
)
|
||||
for did in dashboard_ids
|
||||
]
|
||||
|
||||
app_logger.reflect(
|
||||
"Preview completed",
|
||||
extra={"tables": request.tables, "count": len(result)},
|
||||
)
|
||||
return MaintenanceDashboardPreviewResponse(dashboards=result)
|
||||
# #endregion preview_dashboards
|
||||
|
||||
|
||||
# ── POST /api/maintenance/start ──────────────────────────────
|
||||
# FR-001: Always returns 202 {task_id} after validation
|
||||
# FR-015: operator, admin (NOT viewer)
|
||||
|
||||
@@ -22,6 +22,14 @@ class MaintenanceStartRequest(BaseModel):
|
||||
# #endregion MaintenanceStartRequest
|
||||
|
||||
|
||||
# #region MaintenanceDashboardPreviewRequest [C:1] [TYPE Class]
|
||||
# @BRIEF POST /api/maintenance/preview-dashboards request body.
|
||||
class MaintenanceDashboardPreviewRequest(BaseModel):
|
||||
tables: list[str] = Field(..., min_length=1, max_length=100)
|
||||
environment_id: str = Field(..., description="Target environment for the preview")
|
||||
# #endregion MaintenanceDashboardPreviewRequest
|
||||
|
||||
|
||||
# #region MaintenanceSettingsUpdate [C:1] [TYPE Class]
|
||||
# @BRIEF PUT /api/maintenance/settings request body — all fields optional for partial update.
|
||||
class MaintenanceSettingsUpdate(BaseModel):
|
||||
@@ -114,6 +122,14 @@ class MaintenanceSettingsResponse(BaseModel):
|
||||
# #endregion MaintenanceSettingsResponse
|
||||
|
||||
|
||||
# #region MaintenanceDashboardInfo [C:1] [TYPE Class]
|
||||
# @BRIEF Dashboard ID + title pair for event detail and preview responses.
|
||||
class MaintenanceDashboardInfo(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
# #endregion MaintenanceDashboardInfo
|
||||
|
||||
|
||||
# #region MaintenanceEventItem [C:1] [TYPE Class]
|
||||
# @BRIEF Single maintenance event summary for GET /api/maintenance/events.
|
||||
class MaintenanceEventItem(BaseModel):
|
||||
@@ -124,6 +140,7 @@ class MaintenanceEventItem(BaseModel):
|
||||
message: str | None = None
|
||||
status: str
|
||||
affected_count: int
|
||||
dashboards: list[MaintenanceDashboardInfo] = []
|
||||
created_at: str
|
||||
# #endregion MaintenanceEventItem
|
||||
|
||||
@@ -154,4 +171,11 @@ class MaintenanceAlreadyActiveResponse(BaseModel):
|
||||
status: str = "already_active"
|
||||
# #endregion MaintenanceAlreadyActiveResponse
|
||||
|
||||
|
||||
# #region MaintenanceDashboardPreviewResponse [C:1] [TYPE Class]
|
||||
# @BRIEF Response for POST /api/maintenance/preview-dashboards.
|
||||
class MaintenanceDashboardPreviewResponse(BaseModel):
|
||||
dashboards: list[MaintenanceDashboardInfo]
|
||||
# #endregion MaintenanceDashboardPreviewResponse
|
||||
|
||||
# #endregion MaintenanceSchemasModule
|
||||
|
||||
Reference in New Issue
Block a user