feat: add admin activity timeline filters
This commit is contained in:
+99
-4
@@ -732,14 +732,48 @@ class FileService:
|
|||||||
"updated_at": updated_at,
|
"updated_at": updated_at,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def list_admin_activities(self, limit: int = 8) -> dict[str, Any]:
|
async def list_admin_activities(
|
||||||
limit = min(max(int(limit or 8), 1), self.MAX_ADMIN_ACTIVITIES)
|
self,
|
||||||
|
limit: int = 8,
|
||||||
|
action: Optional[str] = None,
|
||||||
|
target_type: Optional[str] = None,
|
||||||
|
keyword: Optional[str] = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
normalized_limit = int(limit or 8)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
normalized_limit = 8
|
||||||
|
limit = min(max(normalized_limit, 1), self.MAX_ADMIN_ACTIVITIES)
|
||||||
activities = await self._get_admin_activities()
|
activities = await self._get_admin_activities()
|
||||||
visible_activities = activities[:limit]
|
normalized_action = self._normalize_admin_activity_text(action).lower()
|
||||||
|
normalized_target_type = self._normalize_admin_activity_text(target_type).lower()
|
||||||
|
normalized_keyword = self._normalize_admin_activity_text(keyword).lower()
|
||||||
|
filtered_activities = self._filter_admin_activities(
|
||||||
|
activities,
|
||||||
|
action=normalized_action,
|
||||||
|
target_type=normalized_target_type,
|
||||||
|
keyword=normalized_keyword,
|
||||||
|
)
|
||||||
|
visible_activities = filtered_activities[:limit]
|
||||||
|
action_options = self._build_admin_activity_options(activities, "action")
|
||||||
|
target_type_options = self._build_admin_activity_options(activities, "targetType")
|
||||||
return {
|
return {
|
||||||
"activities": visible_activities,
|
"activities": visible_activities,
|
||||||
"items": visible_activities,
|
"items": visible_activities,
|
||||||
"total": len(activities),
|
"total": len(filtered_activities),
|
||||||
|
"storedTotal": len(activities),
|
||||||
|
"stored_total": len(activities),
|
||||||
|
"limit": limit,
|
||||||
|
"filters": {
|
||||||
|
"action": normalized_action,
|
||||||
|
"targetType": normalized_target_type,
|
||||||
|
"target_type": normalized_target_type,
|
||||||
|
"keyword": normalized_keyword,
|
||||||
|
},
|
||||||
|
"actionOptions": action_options,
|
||||||
|
"action_options": action_options,
|
||||||
|
"targetTypeOptions": target_type_options,
|
||||||
|
"target_type_options": target_type_options,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def record_admin_activity(
|
async def record_admin_activity(
|
||||||
@@ -883,6 +917,67 @@ class FileService:
|
|||||||
def _normalize_admin_activity_text(self, value: Any) -> str:
|
def _normalize_admin_activity_text(self, value: Any) -> str:
|
||||||
return str(value or "").strip()[: self.MAX_ADMIN_ACTIVITY_TEXT_LENGTH]
|
return str(value or "").strip()[: self.MAX_ADMIN_ACTIVITY_TEXT_LENGTH]
|
||||||
|
|
||||||
|
def _filter_admin_activities(
|
||||||
|
self,
|
||||||
|
activities: list[dict[str, Any]],
|
||||||
|
action: str,
|
||||||
|
target_type: str,
|
||||||
|
keyword: str,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
filtered_activities = []
|
||||||
|
for activity in activities:
|
||||||
|
if action and str(activity.get("action") or "").lower() != action:
|
||||||
|
continue
|
||||||
|
if target_type and str(activity.get("targetType") or "").lower() != target_type:
|
||||||
|
continue
|
||||||
|
if keyword and not self._activity_matches_keyword(activity, keyword):
|
||||||
|
continue
|
||||||
|
filtered_activities.append(activity)
|
||||||
|
return filtered_activities
|
||||||
|
|
||||||
|
def _activity_matches_keyword(self, activity: dict[str, Any], keyword: str) -> bool:
|
||||||
|
searchable_values = [
|
||||||
|
activity.get("action"),
|
||||||
|
activity.get("targetType"),
|
||||||
|
activity.get("target_type"),
|
||||||
|
activity.get("targetId"),
|
||||||
|
activity.get("target_id"),
|
||||||
|
activity.get("targetName"),
|
||||||
|
activity.get("target_name"),
|
||||||
|
]
|
||||||
|
meta = activity.get("meta")
|
||||||
|
if isinstance(meta, dict):
|
||||||
|
searchable_values.extend(meta.values())
|
||||||
|
|
||||||
|
return any(keyword in str(value or "").lower() for value in searchable_values)
|
||||||
|
|
||||||
|
def _build_admin_activity_options(
|
||||||
|
self,
|
||||||
|
activities: list[dict[str, Any]],
|
||||||
|
field: str,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
counters: dict[str, dict[str, Any]] = {}
|
||||||
|
for activity in activities:
|
||||||
|
raw_value = self._normalize_admin_activity_text(activity.get(field))
|
||||||
|
if not raw_value:
|
||||||
|
continue
|
||||||
|
value = raw_value.lower()
|
||||||
|
if value not in counters:
|
||||||
|
counters[value] = {"label": raw_value, "count": 0}
|
||||||
|
counters[value]["count"] += 1
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"value": value,
|
||||||
|
"label": option["label"],
|
||||||
|
"count": option["count"],
|
||||||
|
}
|
||||||
|
for value, option in sorted(
|
||||||
|
counters.items(),
|
||||||
|
key=lambda item: (-item[1]["count"], item[0]),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
def _build_admin_activity_id(
|
def _build_admin_activity_id(
|
||||||
self,
|
self,
|
||||||
action: str,
|
action: str,
|
||||||
|
|||||||
+19
-1
@@ -4,6 +4,7 @@
|
|||||||
# @Software: PyCharm
|
# @Software: PyCharm
|
||||||
import datetime
|
import datetime
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from apps.admin.services import FileService, ConfigService, LocalFileService
|
from apps.admin.services import FileService, ConfigService, LocalFileService
|
||||||
@@ -39,6 +40,14 @@ admin_api = APIRouter(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_query_text(*values: Optional[str]) -> Optional[str]:
|
||||||
|
for value in values:
|
||||||
|
normalized_value = str(value or "").strip()
|
||||||
|
if normalized_value:
|
||||||
|
return normalized_value
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@admin_api.post("/login")
|
@admin_api.post("/login")
|
||||||
async def login(data: LoginData):
|
async def login(data: LoginData):
|
||||||
if not verify_password(data.password, settings.admin_token):
|
if not verify_password(data.password, settings.admin_token):
|
||||||
@@ -150,9 +159,18 @@ async def dashboard(file_service: FileService = Depends(get_file_service)):
|
|||||||
@admin_api.get("/activities")
|
@admin_api.get("/activities")
|
||||||
async def admin_activities(
|
async def admin_activities(
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
|
action: Optional[str] = None,
|
||||||
|
targetType: Optional[str] = None,
|
||||||
|
target_type: Optional[str] = None,
|
||||||
|
keyword: Optional[str] = None,
|
||||||
file_service: FileService = Depends(get_file_service),
|
file_service: FileService = Depends(get_file_service),
|
||||||
):
|
):
|
||||||
result = await file_service.list_admin_activities(limit=limit)
|
result = await file_service.list_admin_activities(
|
||||||
|
limit=limit,
|
||||||
|
action=action,
|
||||||
|
target_type=_pick_query_text(targetType, target_type),
|
||||||
|
keyword=keyword,
|
||||||
|
)
|
||||||
return APIResponse(detail=result)
|
return APIResponse(detail=result)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user