Новый экранчик для обзора дашей
This commit is contained in:
@@ -146,6 +146,77 @@ def test_get_dashboards_invalid_pagination():
|
||||
# [/DEF:test_get_dashboards_invalid_pagination:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboard_detail_success:Function]
|
||||
# @TEST: GET /api/dashboards/{id} returns dashboard detail with charts and datasets
|
||||
def test_get_dashboard_detail_success():
|
||||
with patch("src.api.routes.dashboards.get_config_manager") as mock_config, \
|
||||
patch("src.api.routes.dashboards.has_permission") as mock_perm, \
|
||||
patch("src.api.routes.dashboards.SupersetClient") as mock_client_cls:
|
||||
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_config.return_value.get_environments.return_value = [mock_env]
|
||||
mock_perm.return_value = lambda: True
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_dashboard_detail.return_value = {
|
||||
"id": 42,
|
||||
"title": "Revenue Dashboard",
|
||||
"slug": "revenue-dashboard",
|
||||
"url": "/superset/dashboard/42/",
|
||||
"description": "Overview",
|
||||
"last_modified": "2026-02-20T10:00:00+00:00",
|
||||
"published": True,
|
||||
"charts": [
|
||||
{
|
||||
"id": 100,
|
||||
"title": "Revenue by Month",
|
||||
"viz_type": "line",
|
||||
"dataset_id": 7,
|
||||
"last_modified": "2026-02-19T10:00:00+00:00",
|
||||
"overview": "line"
|
||||
}
|
||||
],
|
||||
"datasets": [
|
||||
{
|
||||
"id": 7,
|
||||
"table_name": "fact_revenue",
|
||||
"schema": "mart",
|
||||
"database": "Analytics",
|
||||
"last_modified": "2026-02-18T10:00:00+00:00",
|
||||
"overview": "mart.fact_revenue"
|
||||
}
|
||||
],
|
||||
"chart_count": 1,
|
||||
"dataset_count": 1
|
||||
}
|
||||
mock_client_cls.return_value = mock_client
|
||||
|
||||
response = client.get("/api/dashboards/42?env_id=prod")
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["id"] == 42
|
||||
assert payload["chart_count"] == 1
|
||||
assert payload["dataset_count"] == 1
|
||||
# [/DEF:test_get_dashboard_detail_success:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboard_detail_env_not_found:Function]
|
||||
# @TEST: GET /api/dashboards/{id} returns 404 for missing environment
|
||||
def test_get_dashboard_detail_env_not_found():
|
||||
with patch("src.api.routes.dashboards.get_config_manager") as mock_config, \
|
||||
patch("src.api.routes.dashboards.has_permission") as mock_perm:
|
||||
mock_config.return_value.get_environments.return_value = []
|
||||
mock_perm.return_value = lambda: True
|
||||
|
||||
response = client.get("/api/dashboards/42?env_id=missing")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert "Environment not found" in response.json()["detail"]
|
||||
# [/DEF:test_get_dashboard_detail_env_not_found:Function]
|
||||
|
||||
|
||||
# [DEF:test_migrate_dashboards_success:Function]
|
||||
# @TEST: POST /api/dashboards/migrate creates migration task
|
||||
# @PRE: Valid source_env_id, target_env_id, dashboard_ids
|
||||
@@ -283,4 +354,4 @@ def test_get_database_mappings_success():
|
||||
# [/DEF:test_get_database_mappings_success:Function]
|
||||
|
||||
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_dashboards:Module]
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_dashboards:Module]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# @PURPOSE: Contract tests for GET /api/reports/{report_id} detail endpoint behavior.
|
||||
# @LAYER: Domain (Tests)
|
||||
# @RELATION: TESTS -> backend.src.api.routes.reports
|
||||
# @INVARIANT: Detail endpoint tests must keep deterministic assertions for success and not-found contracts.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -16,6 +16,7 @@ from typing import List, Optional, Dict
|
||||
from pydantic import BaseModel, Field
|
||||
from ...dependencies import get_config_manager, get_task_manager, get_resource_service, get_mapping_service, has_permission
|
||||
from ...core.logger import logger, belief_scope
|
||||
from ...core.superset_client import SupersetClient
|
||||
# [/SECTION]
|
||||
|
||||
router = APIRouter(prefix="/api/dashboards", tags=["Dashboards"])
|
||||
@@ -52,6 +53,41 @@ class DashboardsResponse(BaseModel):
|
||||
total_pages: int
|
||||
# [/DEF:DashboardsResponse:DataClass]
|
||||
|
||||
# [DEF:DashboardChartItem:DataClass]
|
||||
class DashboardChartItem(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
viz_type: Optional[str] = None
|
||||
dataset_id: Optional[int] = None
|
||||
last_modified: Optional[str] = None
|
||||
overview: Optional[str] = None
|
||||
# [/DEF:DashboardChartItem:DataClass]
|
||||
|
||||
# [DEF:DashboardDatasetItem:DataClass]
|
||||
class DashboardDatasetItem(BaseModel):
|
||||
id: int
|
||||
table_name: str
|
||||
schema: Optional[str] = None
|
||||
database: str
|
||||
last_modified: Optional[str] = None
|
||||
overview: Optional[str] = None
|
||||
# [/DEF:DashboardDatasetItem:DataClass]
|
||||
|
||||
# [DEF:DashboardDetailResponse:DataClass]
|
||||
class DashboardDetailResponse(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
slug: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
last_modified: Optional[str] = None
|
||||
published: Optional[bool] = None
|
||||
charts: List[DashboardChartItem]
|
||||
datasets: List[DashboardDatasetItem]
|
||||
chart_count: int
|
||||
dataset_count: int
|
||||
# [/DEF:DashboardDetailResponse:DataClass]
|
||||
|
||||
# [DEF:get_dashboards:Function]
|
||||
# @PURPOSE: Fetch list of dashboards from a specific environment with Git status and last task status
|
||||
# @PRE: env_id must be a valid environment ID
|
||||
@@ -132,6 +168,39 @@ async def get_dashboards(
|
||||
raise HTTPException(status_code=503, detail=f"Failed to fetch dashboards: {str(e)}")
|
||||
# [/DEF:get_dashboards:Function]
|
||||
|
||||
# [DEF:get_dashboard_detail:Function]
|
||||
# @PURPOSE: Fetch detailed dashboard info with related charts and datasets
|
||||
# @PRE: env_id must be valid and dashboard_id must exist
|
||||
# @POST: Returns dashboard detail payload for overview page
|
||||
# @RELATION: CALLS -> SupersetClient.get_dashboard_detail
|
||||
@router.get("/{dashboard_id}", response_model=DashboardDetailResponse)
|
||||
async def get_dashboard_detail(
|
||||
dashboard_id: int,
|
||||
env_id: str,
|
||||
config_manager=Depends(get_config_manager),
|
||||
_ = Depends(has_permission("plugin:migration", "READ"))
|
||||
):
|
||||
with belief_scope("get_dashboard_detail", f"dashboard_id={dashboard_id}, env_id={env_id}"):
|
||||
environments = config_manager.get_environments()
|
||||
env = next((e for e in environments if e.id == env_id), None)
|
||||
if not env:
|
||||
logger.error(f"[get_dashboard_detail][Coherence:Failed] Environment not found: {env_id}")
|
||||
raise HTTPException(status_code=404, detail="Environment not found")
|
||||
|
||||
try:
|
||||
client = SupersetClient(env)
|
||||
detail = client.get_dashboard_detail(dashboard_id)
|
||||
logger.info(
|
||||
f"[get_dashboard_detail][Coherence:OK] Dashboard {dashboard_id}: {detail.get('chart_count', 0)} charts, {detail.get('dataset_count', 0)} datasets"
|
||||
)
|
||||
return DashboardDetailResponse(**detail)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"[get_dashboard_detail][Coherence:Failed] Failed to fetch dashboard detail: {e}")
|
||||
raise HTTPException(status_code=503, detail=f"Failed to fetch dashboard detail: {str(e)}")
|
||||
# [/DEF:get_dashboard_detail:Function]
|
||||
|
||||
# [DEF:MigrateRequest:DataClass]
|
||||
class MigrateRequest(BaseModel):
|
||||
source_env_id: str = Field(..., description="Source environment ID")
|
||||
|
||||
Reference in New Issue
Block a user