diff --git a/backend/src/core/superset_client.py b/backend/src/core/superset_client.py index e13e9d6..1f32f84 100644 --- a/backend/src/core/superset_client.py +++ b/backend/src/core/superset_client.py @@ -14,7 +14,7 @@ import json import re import zipfile from pathlib import Path -from typing import Dict, List, Optional, Tuple, Union, cast +from typing import Any, Dict, List, Optional, Tuple, Union, cast from requests import Response from datetime import datetime from .logger import logger as app_logger, belief_scope @@ -87,7 +87,17 @@ class SupersetClient: app_logger.info("[get_dashboards][Enter] Fetching dashboards.") validated_query = self._validate_query_params(query or {}) if 'columns' not in validated_query: - validated_query['columns'] = ["slug", "id", "changed_on_utc", "dashboard_title", "published"] + validated_query['columns'] = [ + "slug", + "id", + "changed_on_utc", + "dashboard_title", + "published", + "created_by", + "changed_by", + "changed_by_name", + "owners", + ] paginated_data = self._fetch_all_pages( endpoint="/dashboard/", @@ -105,53 +115,56 @@ class SupersetClient: # @RETURN: List[Dict] def get_dashboards_summary(self) -> List[Dict]: with belief_scope("SupersetClient.get_dashboards_summary"): - query = { - "columns": [ - "id", - "dashboard_title", - "changed_on_utc", - "published", - "created_by", - "created_by_name", - "changed_by", - "changed_by_name", - "owners", - ] - } + # Rely on list endpoint default projection to stay compatible + # across Superset versions and preserve owners in one request. + query: Dict[str, Any] = {} _, dashboards = self.get_dashboards(query=query) # Map fields to DashboardMetadata schema result = [] for dash in dashboards: + owners = self._extract_owner_labels(dash.get("owners")) + # No per-dashboard detail requests here: keep list endpoint O(1). + if not owners: + owners = self._extract_owner_labels( + [dash.get("created_by"), dash.get("changed_by")], + ) + result.append({ "id": dash.get("id"), "title": dash.get("dashboard_title"), "last_modified": dash.get("changed_on_utc"), "status": "published" if dash.get("published") else "draft", "created_by": self._extract_user_display( - dash.get("created_by_name"), + None, dash.get("created_by"), ), "modified_by": self._extract_user_display( dash.get("changed_by_name"), dash.get("changed_by"), ), - "owners": self._extract_owner_labels(dash.get("owners")), + "owners": owners, }) return result # [/DEF:get_dashboards_summary:Function] # [DEF:_extract_owner_labels:Function] # @PURPOSE: Normalize dashboard owners payload to stable display labels. - # @PRE: owners payload can be None, list of dicts or list of strings. + # @PRE: owners payload can be scalar, object or list. # @POST: Returns deduplicated non-empty owner labels preserving order. # @RETURN: List[str] - def _extract_owner_labels(self, owners_payload: Optional[List[Union[Dict, str]]]) -> List[str]: - if not isinstance(owners_payload, list): + def _extract_owner_labels(self, owners_payload: Any) -> List[str]: + if owners_payload is None: return [] + owners_list: List[Any] + if isinstance(owners_payload, list): + owners_list = owners_payload + else: + owners_list = [owners_payload] + normalized: List[str] = [] - for owner in owners_payload: + for owner in owners_list: label: Optional[str] = None if isinstance(owner, dict): label = self._extract_user_display(None, owner) diff --git a/frontend/src/routes/dashboards/+page.svelte b/frontend/src/routes/dashboards/+page.svelte index e9e396e..b0f61fc 100644 --- a/frontend/src/routes/dashboards/+page.svelte +++ b/frontend/src/routes/dashboards/+page.svelte @@ -69,6 +69,7 @@ let sortColumn = "title"; let sortDirection = "asc"; let openFilterColumn = null; + let filterDropdownPosition = { left: 0, top: 0 }; let columnFilterSearch = { title: "", git_status: "", @@ -482,8 +483,23 @@ * @PRE: column is valid filter key. * @POST: openFilterColumn updated. */ - function toggleFilterDropdown(column) { - openFilterColumn = openFilterColumn === column ? null : column; + function toggleFilterDropdown(column, event, panelWidth = 256) { + event?.stopPropagation(); + if (openFilterColumn === column) { + openFilterColumn = null; + return; + } + const trigger = event?.currentTarget; + if (trigger?.getBoundingClientRect) { + const rect = trigger.getBoundingClientRect(); + const viewportWidth = typeof window !== "undefined" ? window.innerWidth : 1920; + const safeLeft = Math.max(8, Math.min(rect.left, viewportWidth - panelWidth - 8)); + filterDropdownPosition = { + left: safeLeft, + top: rect.bottom + 8, + }; + } + openFilterColumn = column; } // [/DEF:DashboardHub.toggleFilterDropdown:Function] @@ -1212,8 +1228,8 @@ {#if isLoading}