Files
pulp-console/app/views.py
Hyemin Lee abf8654b52 feat: repo list search, header sort, pagination (stage 6)
- list_repos: Pulp next 페이지 끝까지 순회 (첫-페이지-only 누락 수정)
- BFF 메모리 검색(이름)·정렬(이름/검증/버전/패키지/동기화)·페이지네이션(20/페이지)
- GET /repos/table 조각: 검색창은 표 밖(포커스 유지), 정렬헤더/페이저는 표 안,
  hx-include 로 검색어·정렬상태 상호 전송
- 폰트 재조정: 데이터 셀 14px, 헤더 13px, 배지/액션버튼 14px (동기화 버튼 대비 균형)
- 버전 페이지 제목↔검증상세 마진, 데모 저장소 26개로 확장

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:04:54 +09:00

205 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Pulp JSON → 화면용 표시 모델 변환 (순수 함수, httpx 의존 없음).
GPG 검증 배지 로직(✅/⚠️/⚪)은 화면 4 에서도 재사용하므로 여기 함수로 둔다(스펙 §6 화면4).
"""
from __future__ import annotations
from typing import Any
def gpg_status(repo: dict[str, Any]) -> dict[str, str]:
"""repo_config 의 gpgcheck / repo-gpgcheck 로 3단계 검증 배지 산출.
- pass(✅): 패키지 서명 + repo 메타데이터 서명 모두 검증
- warn(⚠️): 둘 중 하나만 검증
- unset(⚪): 검증 미설정
"""
cfg = repo.get("repo_config") or {}
pkg = bool(cfg.get("gpgcheck", 0))
meta = bool(cfg.get("repo-gpgcheck", 0))
# level 은 색상 칩(badge-{level})으로 렌더 — 장식 이모지는 쓰지 않는다(design-ref §7).
if pkg and meta:
return {"level": "pass", "label": "검증됨"}
if pkg or meta:
return {"level": "warn", "label": "부분 검증"}
return {"level": "unset", "label": "미설정"}
def version_number(version_href: str | None) -> int | None:
"""latest_version_href 끝의 정수(vN) 추출. 예: .../versions/3/ → 3"""
if not version_href:
return None
parts = [p for p in version_href.split("/") if p]
try:
return int(parts[-1])
except (ValueError, IndexError):
return None
def _content_count(version: dict[str, Any], section: str) -> int:
"""content_summary[section] (present/added/removed) 의 카운트 합계."""
data = (version.get("content_summary") or {}).get(section) or {}
return sum((info or {}).get("count", 0) for info in data.values())
def package_count(version: dict[str, Any] | None) -> int | None:
"""RepositoryVersion 의 content_summary.present 카운트 합계."""
if version is None:
return None
return _content_count(version, "present")
def _uuid_from_href(href: str) -> str:
parts = [p for p in href.split("/") if p]
return parts[-1] if parts else ""
def build_repo_view(
repo: dict[str, Any], version: dict[str, Any] | None = None
) -> dict[str, Any]:
"""저장소 1건 + (선택) 최신 버전 상세 → 대시보드 행 모델."""
return {
"name": repo.get("name", ""),
"uuid": _uuid_from_href(repo.get("pulp_href", "")),
"href": repo.get("pulp_href", ""),
"latest_version": version_number(repo.get("latest_version_href")),
"package_count": package_count(version),
# 최신 스냅샷 생성 시각 ≈ 마지막 동기화 시각
"last_sync": (version or {}).get("pulp_created"),
"gpg": gpg_status(repo),
# TODO(단계3/4): 현재 운영 배포 버전은 Distribution→publication→version 역추적 필요.
# 단계1에선 최신 버전만 표시한다.
"deployed_version": None,
}
def build_version_view(
version: dict[str, Any],
deployed_number: int | None = None,
gpg: dict[str, str] | None = None,
) -> dict[str, Any]:
"""RepositoryVersion → 버전 목록 행 모델.
gpg 검증은 저장소 단위 설정(repo_config)이라 모든 버전이 같은 값을 공유한다 —
호출부에서 repo 의 gpg_status() 를 주입한다.
"""
number = version_number(version.get("pulp_href"))
is_deployed = number is not None and number == deployed_number
# 배포 가능: 검증 통과(pass) + 현재 배포 중이 아님 (스펙 §5-2 게이트)
deployable = (gpg or {}).get("level") == "pass" and not is_deployed
return {
"number": number,
"href": version.get("pulp_href"),
"created": version.get("pulp_created"),
"package_count": _content_count(version, "present"),
"added": _content_count(version, "added"),
"removed": _content_count(version, "removed"),
"gpg": gpg,
"is_deployed": is_deployed,
"deployable": deployable,
}
def deploy_preview(
version_views: list[dict[str, Any]],
target_number: int | None,
deployed_number: int | None,
) -> dict[str, Any]:
"""배포 확인 모달용 미리보기: 현재 운영 버전 → 대상 버전, 순 패키지 변화."""
def find(n: int | None) -> dict[str, Any] | None:
return next((v for v in version_views if v["number"] == n), None)
current = find(deployed_number)
target = find(target_number)
current_count = current["package_count"] if current else 0
target_count = target["package_count"] if target else 0
net = target_count - current_count
return {
"current_version": deployed_number,
"current_count": current_count,
"target_version": target_number,
"target_count": target_count,
"net": abs(net),
"net_sign": "+" if net >= 0 else "",
}
def verification_detail(repo: dict[str, Any]) -> dict[str, Any]:
"""검증 상태 상세 (화면 4): GPG/checksum 설정값 + 종합 배지."""
cfg = repo.get("repo_config") or {}
return {
"status": gpg_status(repo),
"gpgcheck": int(bool(cfg.get("gpgcheck", 0))),
"repo_gpgcheck": int(bool(cfg.get("repo-gpgcheck", 0))),
"checksum_type": repo.get("metadata_checksum_type")
or cfg.get("checksum_type")
or "",
}
_REPO_SORT_KEYS = {
"name": lambda r: (r["name"] or "").lower(),
"verified": lambda r: {"pass": 0, "warn": 1, "unset": 2}.get(
(r["gpg"] or {}).get("level"), 3
),
"version": lambda r: r["latest_version"] if r["latest_version"] is not None else -1,
"packages": lambda r: r["package_count"] if r["package_count"] is not None else -1,
"last_sync": lambda r: r["last_sync"] or "",
}
def sort_repos(
repo_views: list[dict[str, Any]], key: str, direction: str = "asc"
) -> list[dict[str, Any]]:
"""대시보드 헤더 정렬. 알 수 없는 key 는 이름순으로 폴백."""
fn = _REPO_SORT_KEYS.get(key) or _REPO_SORT_KEYS["name"]
return sorted(repo_views, key=fn, reverse=(direction == "desc"))
def summarize(repo_views: list[dict[str, Any]], syncing: int = 0) -> dict[str, int]:
"""상단 요약 카드 수치. syncing 은 task_store.active_count() 에서 주입."""
return {
"total": len(repo_views),
"syncing": syncing,
"gpg_pass": sum(1 for r in repo_views if r["gpg"]["level"] == "pass"),
"total_packages": sum(r["package_count"] or 0 for r in repo_views),
}
def task_progress(task: dict[str, Any]) -> dict[str, Any]:
"""Pulp task → 진행률 표시 모델.
progress_reports[] 의 done/total 을 합산해 퍼센트를 낸다. 완료 시
created_resources 에서 새 RepositoryVersion 번호(vN)를 추출한다.
"""
state = task.get("state", "waiting")
reports = task.get("progress_reports") or []
done = sum((r or {}).get("done") or 0 for r in reports)
total = sum((r or {}).get("total") or 0 for r in reports)
percent = int(done / total * 100) if total else 0
new_version = None
for href in task.get("created_resources") or []:
if "/versions/" in href:
new_version = version_number(href)
break
error = None
if state == "failed":
err = task.get("error") or {}
error = err.get("description") or "동기화 작업이 실패했습니다."
return {
"state": state,
"done": done,
"total": total,
"percent": percent,
"new_version": new_version,
"error": error,
"completed": state == "completed",
"failed": state == "failed",
"running": state in ("waiting", "running"),
}