feat: add slug-only dashboard profile filter and unify backend imports

This commit is contained in:
2026-03-11 12:20:34 +03:00
parent 50001f5ec5
commit a13f75587d
40 changed files with 376 additions and 149 deletions

3
backend/src/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
# [DEF:src:Package]
# @PURPOSE: Canonical backend package root for application, scripts, and tests.
# [/DEF:src:Package]

View File

@@ -0,0 +1,3 @@
# [DEF:src.api:Package]
# @PURPOSE: Backend API package root.
# [/DEF:src.api:Package]

View File

@@ -82,6 +82,7 @@ def _build_preference_response(user_id: str = "u-1") -> ProfilePreferenceRespons
superset_username="John_Doe",
superset_username_normalized="john_doe",
show_only_my_dashboards=True,
show_only_slug_dashboards=True,
git_username="ivan.ivanov",
git_email="ivan@company.local",
has_git_personal_access_token=True,
@@ -126,6 +127,7 @@ def test_get_profile_preferences_returns_self_payload(profile_route_deps_fixture
assert payload["preference"]["superset_username_normalized"] == "john_doe"
assert payload["preference"]["git_username"] == "ivan.ivanov"
assert payload["preference"]["git_email"] == "ivan@company.local"
assert payload["preference"]["show_only_slug_dashboards"] is True
assert payload["preference"]["has_git_personal_access_token"] is True
assert payload["preference"]["git_personal_access_token_masked"] == "iv***al"
assert payload["preference"]["start_page"] == "reports"
@@ -153,6 +155,7 @@ def test_patch_profile_preferences_success(profile_route_deps_fixture):
json={
"superset_username": "John_Doe",
"show_only_my_dashboards": True,
"show_only_slug_dashboards": True,
"git_username": "ivan.ivanov",
"git_email": "ivan@company.local",
"git_personal_access_token": "ghp_1234567890",
@@ -167,6 +170,7 @@ def test_patch_profile_preferences_success(profile_route_deps_fixture):
assert payload["status"] == "success"
assert payload["preference"]["superset_username"] == "John_Doe"
assert payload["preference"]["show_only_my_dashboards"] is True
assert payload["preference"]["show_only_slug_dashboards"] is True
assert payload["preference"]["git_username"] == "ivan.ivanov"
assert payload["preference"]["git_email"] == "ivan@company.local"
assert payload["preference"]["start_page"] == "reports"
@@ -179,6 +183,7 @@ def test_patch_profile_preferences_success(profile_route_deps_fixture):
assert called_kwargs["payload"].git_username == "ivan.ivanov"
assert called_kwargs["payload"].git_email == "ivan@company.local"
assert called_kwargs["payload"].git_personal_access_token == "ghp_1234567890"
assert called_kwargs["payload"].show_only_slug_dashboards is True
assert called_kwargs["payload"].start_page == "reports-logs"
assert called_kwargs["payload"].auto_open_task_drawer is False
assert called_kwargs["payload"].dashboards_table_density == "free"
@@ -290,4 +295,4 @@ def test_lookup_superset_accounts_env_not_found(profile_route_deps_fixture):
assert payload["detail"] == "Environment 'missing-env' not found"
# [/DEF:test_lookup_superset_accounts_env_not_found:Function]
# [/DEF:backend.src.api.routes.__tests__.test_profile_api:Module]
# [/DEF:backend.src.api.routes.__tests__.test_profile_api:Module]

View File

@@ -98,7 +98,9 @@ class EffectiveProfileFilter(BaseModel):
source_page: Literal["dashboards_main", "other"] = "dashboards_main"
override_show_all: bool = False
username: Optional[str] = None
match_logic: Optional[Literal["owners_or_modified_by"]] = None
match_logic: Optional[
Literal["owners_or_modified_by", "slug_only", "owners_or_modified_by+slug_only"]
] = None
# [/DEF:EffectiveProfileFilter:DataClass]
# [DEF:DashboardsResponse:DataClass]
@@ -535,6 +537,7 @@ async def get_dashboards(
profile_service = ProfileService(db=db, config_manager=config_manager)
bound_username: Optional[str] = None
can_apply_profile_filter = False
can_apply_slug_filter = False
effective_profile_filter = EffectiveProfileFilter(
applied=False,
source_page=page_context,
@@ -560,13 +563,27 @@ async def get_dashboards(
and bool(getattr(profile_preference, "show_only_my_dashboards", False))
and bool(bound_username)
)
can_apply_slug_filter = (
page_context == "dashboards_main"
and bool(apply_profile_default)
and not bool(override_show_all)
and bool(getattr(profile_preference, "show_only_slug_dashboards", True))
)
profile_match_logic = None
if can_apply_profile_filter and can_apply_slug_filter:
profile_match_logic = "owners_or_modified_by+slug_only"
elif can_apply_profile_filter:
profile_match_logic = "owners_or_modified_by"
elif can_apply_slug_filter:
profile_match_logic = "slug_only"
effective_profile_filter = EffectiveProfileFilter(
applied=bool(can_apply_profile_filter),
applied=bool(can_apply_profile_filter or can_apply_slug_filter),
source_page=page_context,
override_show_all=bool(override_show_all),
username=bound_username if can_apply_profile_filter else None,
match_logic="owners_or_modified_by" if can_apply_profile_filter else None,
match_logic=profile_match_logic,
)
except Exception as profile_error:
logger.explore(
@@ -589,7 +606,7 @@ async def get_dashboards(
actor_filters,
)
)
needs_full_scan = has_column_filters or bool(can_apply_profile_filter)
needs_full_scan = has_column_filters or bool(can_apply_profile_filter) or bool(can_apply_slug_filter)
if isinstance(resource_service, ResourceService) and not needs_full_scan:
try:
@@ -600,6 +617,7 @@ async def get_dashboards(
page_size=page_size,
search=search,
include_git_status=False,
require_slug=bool(can_apply_slug_filter),
)
paginated_dashboards = page_payload["dashboards"]
total = page_payload["total"]
@@ -613,6 +631,7 @@ async def get_dashboards(
env,
all_tasks,
include_git_status=False,
require_slug=bool(can_apply_slug_filter),
)
if search:
@@ -633,6 +652,7 @@ async def get_dashboards(
env,
all_tasks,
include_git_status=bool(git_filters),
require_slug=bool(can_apply_slug_filter),
)
if can_apply_profile_filter and bound_username:
@@ -674,6 +694,13 @@ async def get_dashboards(
)
dashboards = filtered_dashboards
if can_apply_slug_filter:
dashboards = [
dashboard
for dashboard in dashboards
if str(dashboard.get("slug") or "").strip()
]
if search:
search_lower = search.lower()
dashboards = [

View File

@@ -0,0 +1,3 @@
# [DEF:src.core:Package]
# @PURPOSE: Backend core services and infrastructure package root.
# [/DEF:src.core:Package]

View File

@@ -0,0 +1,3 @@
# [DEF:src.core.auth:Package]
# @PURPOSE: Authentication and authorization package root.
# [/DEF:src.core.auth:Package]

View File

@@ -141,6 +141,11 @@ def _ensure_user_dashboard_preferences_columns(bind_engine):
"ALTER TABLE user_dashboard_preferences "
"ADD COLUMN dashboards_table_density VARCHAR NOT NULL DEFAULT 'comfortable'"
)
if "show_only_slug_dashboards" not in existing_columns:
alter_statements.append(
"ALTER TABLE user_dashboard_preferences "
"ADD COLUMN show_only_slug_dashboards BOOLEAN NOT NULL DEFAULT TRUE"
)
if not alter_statements:
return

View File

@@ -76,17 +76,8 @@ class PluginLoader:
"""
Loads a single Python module and extracts PluginBase subclasses.
"""
# Try to determine the correct package prefix based on how the app is running
# For standalone execution, we need to handle the import differently
if __name__ == "__main__" or "test" in __name__:
# When running as standalone or in tests, use relative import
package_name = f"plugins.{module_name}"
elif "backend.src" in __name__:
package_prefix = "backend.src.plugins"
package_name = f"{package_prefix}.{module_name}"
else:
package_prefix = "src.plugins"
package_name = f"{package_prefix}.{module_name}"
# All runtime code is imported through the canonical `src` package root.
package_name = f"src.plugins.{module_name}"
# print(f"DEBUG: Loading plugin {module_name} as {package_name}")
spec = importlib.util.spec_from_file_location(package_name, file_path)
@@ -198,4 +189,4 @@ class PluginLoader:
return plugin_id in self._plugins
# [/DEF:has_plugin:Function]
# [/DEF:PluginLoader:Class]
# [/DEF:PluginLoader:Class]

View File

@@ -150,11 +150,19 @@ class SupersetClient:
# @PRE: Client is authenticated.
# @POST: Returns a list of dashboard metadata summaries.
# @RETURN: List[Dict]
def get_dashboards_summary(self) -> List[Dict]:
def get_dashboards_summary(self, require_slug: bool = False) -> List[Dict]:
with belief_scope("SupersetClient.get_dashboards_summary"):
# Rely on list endpoint default projection to stay compatible
# across Superset versions and preserve owners in one request.
query: Dict[str, Any] = {}
if require_slug:
query["filters"] = [
{
"col": "slug",
"opr": "neq",
"value": "",
}
]
_, dashboards = self.get_dashboards(query=query)
# Map fields to DashboardMetadata schema
@@ -232,23 +240,35 @@ class SupersetClient:
page: int,
page_size: int,
search: Optional[str] = None,
require_slug: bool = False,
) -> Tuple[int, List[Dict]]:
with belief_scope("SupersetClient.get_dashboards_summary_page"):
query: Dict[str, Any] = {
"page": max(page - 1, 0),
"page_size": page_size,
}
filters: List[Dict[str, Any]] = []
if require_slug:
filters.append(
{
"col": "slug",
"opr": "neq",
"value": "",
}
)
normalized_search = (search or "").strip()
if normalized_search:
# Superset list API supports filter objects with `opr` operator.
# `ct` -> contains (ILIKE on most Superset backends).
query["filters"] = [
filters.append(
{
"col": "dashboard_title",
"opr": "ct",
"value": normalized_search,
}
]
)
if filters:
query["filters"] = filters
total_count, dashboards = self.get_dashboards_page(query=query)

View File

@@ -0,0 +1,3 @@
# [DEF:src.core.utils:Package]
# @PURPOSE: Shared utility package root.
# [/DEF:src.core.utils:Package]

View File

@@ -0,0 +1,3 @@
# [DEF:src.models:Package]
# @PURPOSE: Domain model package root.
# [/DEF:src.models:Package]

View File

@@ -32,6 +32,7 @@ class UserDashboardPreference(Base):
superset_username_normalized = Column(String, nullable=True, index=True)
show_only_my_dashboards = Column(Boolean, nullable=False, default=False)
show_only_slug_dashboards = Column(Boolean, nullable=False, default=True)
git_username = Column(String, nullable=True)
git_email = Column(String, nullable=True)
@@ -56,4 +57,4 @@ class UserDashboardPreference(Base):
user = relationship("User")
# [/DEF:UserDashboardPreference:Class]
# [/DEF:backend.src.models.profile:Module]
# [/DEF:backend.src.models.profile:Module]

View File

@@ -0,0 +1,3 @@
# [DEF:src.plugins:Package]
# @PURPOSE: Plugin package root for dynamic discovery and runtime imports.
# [/DEF:src.plugins:Package]

View File

@@ -0,0 +1,3 @@
# [DEF:src.plugins.git:Package]
# @PURPOSE: Git plugin extension package root.
# [/DEF:src.plugins.git:Package]

View File

@@ -0,0 +1,3 @@
# [DEF:src.schemas:Package]
# @PURPOSE: API schema package root.
# [/DEF:src.schemas:Package]

View File

@@ -45,6 +45,7 @@ class ProfilePreference(BaseModel):
superset_username: Optional[str] = None
superset_username_normalized: Optional[str] = None
show_only_my_dashboards: bool = False
show_only_slug_dashboards: bool = True
git_username: Optional[str] = None
git_email: Optional[str] = None
@@ -79,6 +80,10 @@ class ProfilePreferenceUpdateRequest(BaseModel):
default=None,
description='When true, "/dashboards" can auto-apply profile filter in main context.',
)
show_only_slug_dashboards: Optional[bool] = Field(
default=None,
description='When true, "/dashboards" hides dashboards without slug by default.',
)
git_username: Optional[str] = Field(
default=None,
description="Git author username used for commit signature.",
@@ -172,4 +177,4 @@ class SupersetAccountLookupResponse(BaseModel):
items: List[SupersetAccountCandidate] = Field(default_factory=list)
# [/DEF:SupersetAccountLookupResponse:Class]
# [/DEF:backend.src.schemas.profile:Module]
# [/DEF:backend.src.schemas.profile:Module]

View File

@@ -0,0 +1,3 @@
# [DEF:src.scripts:Package]
# @PURPOSE: Script entrypoint package root.
# [/DEF:src.scripts:Package]

View File

@@ -15,13 +15,13 @@ from datetime import datetime, timezone
from types import SimpleNamespace
from typing import List, Optional, Any, Dict
# Standardize sys.path for direct execution from project root or scripts dir
# Standardize sys.path for direct execution from project root or scripts dir.
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "..", ".."))
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
BACKEND_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "..", ".."))
if BACKEND_ROOT not in sys.path:
sys.path.insert(0, BACKEND_ROOT)
from backend.src.models.clean_release import (
from src.models.clean_release import (
CandidateArtifact,
CheckFinalStatus,
CheckStageName,
@@ -35,12 +35,12 @@ from backend.src.models.clean_release import (
RegistryStatus,
ReleaseCandidateStatus,
)
from backend.src.services.clean_release.approval_service import approve_candidate
from backend.src.services.clean_release.compliance_execution_service import ComplianceExecutionService
from backend.src.services.clean_release.enums import CandidateStatus
from backend.src.services.clean_release.manifest_service import build_manifest_snapshot
from backend.src.services.clean_release.publication_service import publish_candidate
from backend.src.services.clean_release.repository import CleanReleaseRepository
from src.services.clean_release.approval_service import approve_candidate
from src.services.clean_release.compliance_execution_service import ComplianceExecutionService
from src.services.clean_release.enums import CandidateStatus
from src.services.clean_release.manifest_service import build_manifest_snapshot
from src.services.clean_release.publication_service import publish_candidate
from src.services.clean_release.repository import CleanReleaseRepository
# [DEF:TuiFacadeAdapter:Class]
# @PURPOSE: Thin TUI adapter that routes business mutations through application services.

View File

@@ -0,0 +1,3 @@
# [DEF:src.services.notifications:Package]
# @PURPOSE: Notification service package root.
# [/DEF:src.services.notifications:Package]

View File

@@ -145,6 +145,14 @@ class ProfileService:
if "show_only_my_dashboards" in provided_fields:
effective_show_only = bool(payload.show_only_my_dashboards)
effective_show_only_slug = (
bool(preference.show_only_slug_dashboards)
if preference.show_only_slug_dashboards is not None
else True
)
if "show_only_slug_dashboards" in provided_fields:
effective_show_only_slug = bool(payload.show_only_slug_dashboards)
effective_git_username = self._sanitize_text(preference.git_username)
if "git_username" in provided_fields:
effective_git_username = self._sanitize_text(payload.git_username)
@@ -206,6 +214,7 @@ class ProfileService:
effective_superset_username
)
preference.show_only_my_dashboards = effective_show_only
preference.show_only_slug_dashboards = effective_show_only_slug
preference.git_username = effective_git_username
preference.git_email = effective_git_email
@@ -460,6 +469,11 @@ class ProfileService:
preference.superset_username_normalized
),
show_only_my_dashboards=bool(preference.show_only_my_dashboards),
show_only_slug_dashboards=(
bool(preference.show_only_slug_dashboards)
if preference.show_only_slug_dashboards is not None
else True
),
git_username=self._sanitize_text(preference.git_username),
git_email=self._sanitize_text(preference.git_email),
has_git_personal_access_token=bool(encrypted_token),
@@ -586,6 +600,7 @@ class ProfileService:
superset_username=None,
superset_username_normalized=None,
show_only_my_dashboards=False,
show_only_slug_dashboards=True,
git_username=None,
git_email=None,
has_git_personal_access_token=False,
@@ -709,4 +724,4 @@ class ProfileService:
# [/DEF:_normalize_owner_tokens:Function]
# [/DEF:ProfileService:Class]
# [/DEF:backend.src.services.profile_service:Module]
# [/DEF:backend.src.services.profile_service:Module]

View File

@@ -0,0 +1,3 @@
# [DEF:src.services.reports:Package]
# @PURPOSE: Report service package root.
# [/DEF:src.services.reports:Package]

View File

@@ -46,10 +46,11 @@ class ResourceService:
env: Any,
tasks: Optional[List[Task]] = None,
include_git_status: bool = True,
require_slug: bool = False,
) -> List[Dict[str, Any]]:
with belief_scope("get_dashboards_with_status", f"env={env.id}"):
client = SupersetClient(env)
dashboards = client.get_dashboards_summary()
dashboards = client.get_dashboards_summary(require_slug=require_slug)
# Enhance each dashboard with Git status and task status
result = []
@@ -96,6 +97,7 @@ class ResourceService:
page_size: int = 10,
search: Optional[str] = None,
include_git_status: bool = True,
require_slug: bool = False,
) -> Dict[str, Any]:
with belief_scope(
"get_dashboards_page_with_status",
@@ -106,6 +108,7 @@ class ResourceService:
page=page,
page_size=page_size,
search=search,
require_slug=require_slug,
)
result = []