feat: add slug-only dashboard profile filter and unify backend imports
This commit is contained in:
3
backend/src/__init__.py
Normal file
3
backend/src/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# [DEF:src:Package]
|
||||
# @PURPOSE: Canonical backend package root for application, scripts, and tests.
|
||||
# [/DEF:src:Package]
|
||||
3
backend/src/api/__init__.py
Normal file
3
backend/src/api/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# [DEF:src.api:Package]
|
||||
# @PURPOSE: Backend API package root.
|
||||
# [/DEF:src.api:Package]
|
||||
@@ -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]
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
3
backend/src/core/__init__.py
Normal file
3
backend/src/core/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# [DEF:src.core:Package]
|
||||
# @PURPOSE: Backend core services and infrastructure package root.
|
||||
# [/DEF:src.core:Package]
|
||||
3
backend/src/core/auth/__init__.py
Normal file
3
backend/src/core/auth/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# [DEF:src.core.auth:Package]
|
||||
# @PURPOSE: Authentication and authorization package root.
|
||||
# [/DEF:src.core.auth:Package]
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
3
backend/src/core/utils/__init__.py
Normal file
3
backend/src/core/utils/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# [DEF:src.core.utils:Package]
|
||||
# @PURPOSE: Shared utility package root.
|
||||
# [/DEF:src.core.utils:Package]
|
||||
3
backend/src/models/__init__.py
Normal file
3
backend/src/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# [DEF:src.models:Package]
|
||||
# @PURPOSE: Domain model package root.
|
||||
# [/DEF:src.models:Package]
|
||||
@@ -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]
|
||||
|
||||
3
backend/src/plugins/__init__.py
Normal file
3
backend/src/plugins/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# [DEF:src.plugins:Package]
|
||||
# @PURPOSE: Plugin package root for dynamic discovery and runtime imports.
|
||||
# [/DEF:src.plugins:Package]
|
||||
3
backend/src/plugins/git/__init__.py
Normal file
3
backend/src/plugins/git/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# [DEF:src.plugins.git:Package]
|
||||
# @PURPOSE: Git plugin extension package root.
|
||||
# [/DEF:src.plugins.git:Package]
|
||||
3
backend/src/schemas/__init__.py
Normal file
3
backend/src/schemas/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# [DEF:src.schemas:Package]
|
||||
# @PURPOSE: API schema package root.
|
||||
# [/DEF:src.schemas:Package]
|
||||
@@ -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]
|
||||
|
||||
3
backend/src/scripts/__init__.py
Normal file
3
backend/src/scripts/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# [DEF:src.scripts:Package]
|
||||
# @PURPOSE: Script entrypoint package root.
|
||||
# [/DEF:src.scripts:Package]
|
||||
@@ -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.
|
||||
|
||||
3
backend/src/services/notifications/__init__.py
Normal file
3
backend/src/services/notifications/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# [DEF:src.services.notifications:Package]
|
||||
# @PURPOSE: Notification service package root.
|
||||
# [/DEF:src.services.notifications:Package]
|
||||
@@ -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]
|
||||
|
||||
3
backend/src/services/reports/__init__.py
Normal file
3
backend/src/services/reports/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# [DEF:src.services.reports:Package]
|
||||
# @PURPOSE: Report service package root.
|
||||
# [/DEF:src.services.reports:Package]
|
||||
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user