generated from coulomb/repo-seed
Count badges now with navigation
This commit is contained in:
@@ -464,6 +464,11 @@ def repository_detail(
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
runs = service.list_analysis_runs(repository_id)
|
||||
ability_map = service.ability_map(repository_id)
|
||||
latest_candidate = latest_completed_candidate_graph(
|
||||
service,
|
||||
repository_id,
|
||||
runs,
|
||||
)
|
||||
decisions = service.list_review_decisions(repository_id)
|
||||
facts = service.list_observed_facts(repository_id)
|
||||
languages = sorted({fact.name for fact in facts if fact.kind == "language"})
|
||||
@@ -517,7 +522,13 @@ def repository_detail(
|
||||
</section>
|
||||
<section class="panel">
|
||||
<h2>Approved Ability Map</h2>
|
||||
{render_graph_counts(asdict(ability_map), facts_count=None)}
|
||||
{render_graph_counts(
|
||||
asdict(ability_map),
|
||||
facts_count=None,
|
||||
base_href=f"/ui/repos/{repository_id}/elements?scope=approved",
|
||||
)}
|
||||
<h2>Latest Candidate Graph</h2>
|
||||
{render_latest_candidate_counts(repository_id, latest_candidate, service)}
|
||||
{render_ability_map(asdict(ability_map), repository_id)}
|
||||
</section>
|
||||
</div>
|
||||
@@ -873,7 +884,18 @@ def analysis_run_detail(
|
||||
<section class="panel">
|
||||
<div class="actions">
|
||||
<h2 style="margin-right:auto">Candidate Graph</h2>
|
||||
{render_graph_counts(asdict(candidate_graph), facts_count=len(facts))}
|
||||
{render_graph_counts(
|
||||
asdict(candidate_graph),
|
||||
facts_count=len(facts),
|
||||
base_href=(
|
||||
f"/ui/repos/{repository_id}/elements?scope=candidate"
|
||||
f"&analysis_run_id={analysis_run_id}"
|
||||
),
|
||||
facts_href=(
|
||||
f"/ui/repos/{repository_id}/elements?scope=facts"
|
||||
f"&analysis_run_id={analysis_run_id}&type=facts"
|
||||
),
|
||||
)}
|
||||
<form method="post" action="/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph/approve">
|
||||
<button type="submit">Approve</button>
|
||||
</form>
|
||||
@@ -883,7 +905,15 @@ def analysis_run_detail(
|
||||
<section class="panel">
|
||||
<div class="actions">
|
||||
<h2 style="margin-right:auto">Observed Facts</h2>
|
||||
{render_count_pills(facts=len(facts))}
|
||||
{render_count_pills(
|
||||
hrefs={
|
||||
"facts": (
|
||||
f"/ui/repos/{repository_id}/elements?scope=facts"
|
||||
f"&analysis_run_id={analysis_run_id}&type=facts"
|
||||
)
|
||||
},
|
||||
facts=len(facts),
|
||||
)}
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Kind</th><th>Name</th><th>Path</th><th>Value</th></tr></thead>
|
||||
@@ -901,6 +931,52 @@ def analysis_run_detail(
|
||||
return page(f"{repository.name} Run {analysis_run_id}", body)
|
||||
|
||||
|
||||
@router.get("/ui/repos/{repository_id}/elements")
|
||||
def repository_element_listing(
|
||||
repository_id: int,
|
||||
scope: str = Query("approved"),
|
||||
type: str = Query("abilities"),
|
||||
analysis_run_id: int | None = Query(default=None),
|
||||
service: RegistryService = Depends(get_service),
|
||||
) -> HTMLResponse:
|
||||
repository = service.get_repository(repository_id)
|
||||
title = element_listing_title(repository.name, scope, type)
|
||||
if scope == "approved":
|
||||
graph = asdict(service.ability_map(repository_id))
|
||||
rows = render_graph_element_rows(graph, type)
|
||||
elif scope == "candidate":
|
||||
if analysis_run_id is None:
|
||||
runs = service.list_analysis_runs(repository_id)
|
||||
latest = latest_completed_candidate_graph(service, repository_id, runs)
|
||||
if latest is None:
|
||||
rows = '<tr><td class="muted">No completed candidate graph.</td></tr>'
|
||||
else:
|
||||
analysis_run_id, candidate_graph = latest
|
||||
rows = render_graph_element_rows(asdict(candidate_graph), type)
|
||||
else:
|
||||
candidate_graph = service.candidate_graph(repository_id, analysis_run_id)
|
||||
rows = render_graph_element_rows(asdict(candidate_graph), type)
|
||||
elif scope == "facts":
|
||||
facts = service.list_observed_facts(repository_id, analysis_run_id)
|
||||
rows = render_fact_element_rows(facts)
|
||||
else:
|
||||
rows = '<tr><td class="muted">Unknown listing scope.</td></tr>'
|
||||
|
||||
body = f"""
|
||||
<div class="actions">
|
||||
<h1 style="margin-right:auto">{escape(title)}</h1>
|
||||
<a class="button secondary" href="/ui/repos/{repository_id}">Repository</a>
|
||||
</div>
|
||||
<section class="panel">
|
||||
<table>
|
||||
<thead><tr><th>Type</th><th>Name</th><th>Parent</th><th>Source</th></tr></thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</table>
|
||||
</section>
|
||||
"""
|
||||
return page(title, body)
|
||||
|
||||
|
||||
@router.get("/ui/repos/{repository_id}/analysis-runs/{base_analysis_run_id}/diff/{target_analysis_run_id}")
|
||||
def analysis_run_diff_detail(
|
||||
repository_id: int,
|
||||
@@ -1573,7 +1649,52 @@ def split_capability_lines(value: str) -> list[str]:
|
||||
return [line.strip() for line in normalized.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def render_graph_counts(graph: dict, facts_count: int | None = None) -> str:
|
||||
def latest_completed_candidate_graph(
|
||||
service: RegistryService,
|
||||
repository_id: int,
|
||||
runs: list,
|
||||
):
|
||||
for run in sorted(runs, key=lambda item: item.id, reverse=True):
|
||||
if run.status != "completed":
|
||||
continue
|
||||
try:
|
||||
return run.id, service.candidate_graph(repository_id, run.id)
|
||||
except NotFoundError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def render_latest_candidate_counts(
|
||||
repository_id: int,
|
||||
latest_candidate,
|
||||
service: RegistryService,
|
||||
) -> str:
|
||||
if latest_candidate is None:
|
||||
return '<p class="muted">No completed candidate graph yet.</p>'
|
||||
analysis_run_id, candidate_graph = latest_candidate
|
||||
facts_count = len(service.list_observed_facts(repository_id, analysis_run_id))
|
||||
return render_graph_counts(
|
||||
asdict(candidate_graph),
|
||||
facts_count=facts_count,
|
||||
label_prefix="candidate",
|
||||
base_href=(
|
||||
f"/ui/repos/{repository_id}/elements?scope=candidate"
|
||||
f"&analysis_run_id={analysis_run_id}"
|
||||
),
|
||||
facts_href=(
|
||||
f"/ui/repos/{repository_id}/elements?scope=facts"
|
||||
f"&analysis_run_id={analysis_run_id}&type=facts"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def render_graph_counts(
|
||||
graph: dict,
|
||||
facts_count: int | None = None,
|
||||
label_prefix: str = "",
|
||||
base_href: str | None = None,
|
||||
facts_href: str | None = None,
|
||||
) -> str:
|
||||
abilities = graph.get("abilities", [])
|
||||
capabilities = [
|
||||
capability
|
||||
@@ -1592,20 +1713,121 @@ def render_graph_counts(graph: dict, facts_count: int | None = None) -> str:
|
||||
}
|
||||
if facts_count is not None:
|
||||
counts["facts"] = facts_count
|
||||
return render_count_pills(**counts)
|
||||
hrefs = (
|
||||
{name: f"{base_href}&type={name}" for name in counts if name != "facts"}
|
||||
if base_href
|
||||
else {}
|
||||
)
|
||||
if facts_href:
|
||||
hrefs["facts"] = facts_href
|
||||
return render_count_pills(label_prefix=label_prefix, hrefs=hrefs, **counts)
|
||||
|
||||
|
||||
def render_count_pills(**counts: int) -> str:
|
||||
def render_count_pills(
|
||||
label_prefix: str = "",
|
||||
hrefs: dict[str, str] | None = None,
|
||||
**counts: int,
|
||||
) -> str:
|
||||
labels = {
|
||||
"abilities": "abilities",
|
||||
"capabilities": "capabilities",
|
||||
"features": "features",
|
||||
"facts": "facts",
|
||||
}
|
||||
return "".join(
|
||||
f'<span class="pill">{count} {labels[name]}</span>'
|
||||
for name, count in counts.items()
|
||||
)
|
||||
prefix = f"{label_prefix} " if label_prefix else ""
|
||||
hrefs = hrefs or {}
|
||||
items = []
|
||||
for name, count in counts.items():
|
||||
label = f"{count} {prefix}{labels[name]}"
|
||||
if name in hrefs:
|
||||
items.append(f'<a class="pill" href="{escape(hrefs[name])}">{label}</a>')
|
||||
else:
|
||||
items.append(f'<span class="pill">{label}</span>')
|
||||
return "".join(items)
|
||||
|
||||
|
||||
def render_graph_element_rows(graph: dict, item_type: str) -> str:
|
||||
rows: list[str] = []
|
||||
for ability in graph.get("abilities", []):
|
||||
if item_type == "abilities":
|
||||
rows.append(
|
||||
render_element_row("ability", ability["name"], "", ability.get("source_refs", []))
|
||||
)
|
||||
for capability in ability.get("capabilities", []):
|
||||
if item_type == "capabilities":
|
||||
rows.append(
|
||||
render_element_row(
|
||||
"capability",
|
||||
capability["name"],
|
||||
ability["name"],
|
||||
capability.get("source_refs", []),
|
||||
)
|
||||
)
|
||||
for feature in capability.get("features", []):
|
||||
if item_type == "features":
|
||||
rows.append(
|
||||
render_element_row(
|
||||
"feature",
|
||||
feature["name"],
|
||||
capability["name"],
|
||||
feature.get("source_refs", []),
|
||||
)
|
||||
)
|
||||
return "\n".join(rows) or '<tr><td colspan="4" class="muted">No matching elements.</td></tr>'
|
||||
|
||||
|
||||
def element_listing_title(repository_name: str, scope: str, item_type: str) -> str:
|
||||
type_labels = {
|
||||
"abilities": "Abilities",
|
||||
"capabilities": "Capabilities",
|
||||
"features": "Features",
|
||||
"facts": "Facts",
|
||||
}
|
||||
if scope == "facts":
|
||||
return f"{repository_name} · Observed Facts"
|
||||
scope_labels = {
|
||||
"approved": "Approved",
|
||||
"candidate": "Candidate",
|
||||
}
|
||||
scope_label = scope_labels.get(scope, scope.title())
|
||||
type_label = type_labels.get(item_type, item_type.title())
|
||||
return f"{repository_name} · {scope_label} {type_label}"
|
||||
|
||||
|
||||
def render_fact_element_rows(facts: list) -> str:
|
||||
rows = [
|
||||
render_element_row(
|
||||
fact.kind,
|
||||
fact.name,
|
||||
fact.path,
|
||||
[
|
||||
{
|
||||
"path": fact.path,
|
||||
"kind": fact.kind,
|
||||
"name": fact.name,
|
||||
"line": fact.metadata.get("line"),
|
||||
}
|
||||
],
|
||||
)
|
||||
for fact in facts
|
||||
]
|
||||
return "\n".join(rows) or '<tr><td colspan="4" class="muted">No matching facts.</td></tr>'
|
||||
|
||||
|
||||
def render_element_row(
|
||||
item_type: str,
|
||||
name: str,
|
||||
parent: str,
|
||||
source_refs: list[dict],
|
||||
) -> str:
|
||||
return f"""
|
||||
<tr>
|
||||
<td>{escape(item_type)}</td>
|
||||
<td>{escape(name)}</td>
|
||||
<td>{escape(parent)}</td>
|
||||
<td>{render_sources(source_refs)}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
|
||||
def render_candidate_graph(graph: dict, repository_id: int, analysis_run_id: int) -> str:
|
||||
|
||||
Reference in New Issue
Block a user