generated from coulomb/repo-seed
Classification for abilities, capabilities, features and filters for list view
This commit is contained in:
@@ -936,6 +936,8 @@ def repository_element_listing(
|
||||
repository_id: int,
|
||||
scope: str = Query("approved"),
|
||||
type: str = Query("abilities"),
|
||||
q: str = Query(""),
|
||||
class_filter: str = Query(""),
|
||||
analysis_run_id: int | None = Query(default=None),
|
||||
service: RegistryService = Depends(get_service),
|
||||
) -> HTMLResponse:
|
||||
@@ -943,33 +945,53 @@ def repository_element_listing(
|
||||
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)
|
||||
elements = 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>'
|
||||
elements = []
|
||||
else:
|
||||
analysis_run_id, candidate_graph = latest
|
||||
rows = render_graph_element_rows(asdict(candidate_graph), type)
|
||||
elements = 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)
|
||||
elements = 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)
|
||||
elements = fact_element_rows(facts)
|
||||
else:
|
||||
rows = '<tr><td class="muted">Unknown listing scope.</td></tr>'
|
||||
elements = []
|
||||
filtered = filter_element_rows(elements, q, class_filter)
|
||||
rows = render_element_rows(filtered)
|
||||
filter_action = f"/ui/repos/{repository_id}/elements"
|
||||
|
||||
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" style="margin-bottom:18px">
|
||||
<form class="stack" method="get" action="{filter_action}">
|
||||
<input type="hidden" name="scope" value="{escape(scope)}">
|
||||
<input type="hidden" name="type" value="{escape(type)}">
|
||||
{render_optional_hidden("analysis_run_id", analysis_run_id)}
|
||||
<div class="grid">
|
||||
<label>Search <input name="q" value="{escape(q)}" placeholder="Name, parent, source, or class"></label>
|
||||
<label>Class <input name="class_filter" value="{escape(class_filter)}" list="element-classes" placeholder="Any class"></label>
|
||||
</div>
|
||||
{render_class_datalist(elements)}
|
||||
<div class="actions">
|
||||
<button type="submit">Filter</button>
|
||||
<a class="button secondary" href="{filter_action}?scope={escape(scope)}&type={escape(type)}{render_analysis_run_query_suffix(analysis_run_id)}">Clear</a>
|
||||
<span class="muted">{len(filtered)} of {len(elements)} shown</span>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<table>
|
||||
<thead><tr><th>Type</th><th>Name</th><th>Parent</th><th>Source</th></tr></thead>
|
||||
<thead><tr><th>Class</th><th>Name</th><th>Parent</th><th>Source</th></tr></thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</table>
|
||||
</section>
|
||||
@@ -1746,18 +1768,23 @@ def render_count_pills(
|
||||
return "".join(items)
|
||||
|
||||
|
||||
def render_graph_element_rows(graph: dict, item_type: str) -> str:
|
||||
rows: list[str] = []
|
||||
def graph_element_rows(graph: dict, item_type: str) -> list[dict]:
|
||||
rows: list[dict] = []
|
||||
for ability in graph.get("abilities", []):
|
||||
if item_type == "abilities":
|
||||
rows.append(
|
||||
render_element_row("ability", ability["name"], "", ability.get("source_refs", []))
|
||||
element_row(
|
||||
ability.get("primary_class", "ability"),
|
||||
ability["name"],
|
||||
"",
|
||||
ability.get("source_refs", []),
|
||||
)
|
||||
)
|
||||
for capability in ability.get("capabilities", []):
|
||||
if item_type == "capabilities":
|
||||
rows.append(
|
||||
render_element_row(
|
||||
"capability",
|
||||
element_row(
|
||||
capability.get("primary_class", "capability"),
|
||||
capability["name"],
|
||||
ability["name"],
|
||||
capability.get("source_refs", []),
|
||||
@@ -1766,14 +1793,14 @@ def render_graph_element_rows(graph: dict, item_type: str) -> str:
|
||||
for feature in capability.get("features", []):
|
||||
if item_type == "features":
|
||||
rows.append(
|
||||
render_element_row(
|
||||
"feature",
|
||||
element_row(
|
||||
feature.get("type", "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>'
|
||||
return rows
|
||||
|
||||
|
||||
def element_listing_title(repository_name: str, scope: str, item_type: str) -> str:
|
||||
@@ -1794,9 +1821,9 @@ def element_listing_title(repository_name: str, scope: str, item_type: str) -> s
|
||||
return f"{repository_name} · {scope_label} {type_label}"
|
||||
|
||||
|
||||
def render_fact_element_rows(facts: list) -> str:
|
||||
rows = [
|
||||
render_element_row(
|
||||
def fact_element_rows(facts: list) -> list[dict]:
|
||||
return [
|
||||
element_row(
|
||||
fact.kind,
|
||||
fact.name,
|
||||
fact.path,
|
||||
@@ -1811,25 +1838,96 @@ def render_fact_element_rows(facts: list) -> str:
|
||||
)
|
||||
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,
|
||||
def element_row(
|
||||
primary_class: str,
|
||||
name: str,
|
||||
parent: str,
|
||||
source_refs: list[dict],
|
||||
) -> str:
|
||||
) -> dict:
|
||||
return {
|
||||
"primary_class": primary_class,
|
||||
"name": name,
|
||||
"parent": parent,
|
||||
"source_refs": source_refs,
|
||||
}
|
||||
|
||||
|
||||
def filter_element_rows(
|
||||
rows: list[dict],
|
||||
query: str,
|
||||
class_filter: str,
|
||||
) -> list[dict]:
|
||||
query = query.strip().lower()
|
||||
class_filter = class_filter.strip().lower()
|
||||
filtered = []
|
||||
for row in rows:
|
||||
row_class = str(row["primary_class"]).lower()
|
||||
if class_filter and class_filter not in row_class:
|
||||
continue
|
||||
haystack = " ".join(
|
||||
[
|
||||
str(row["primary_class"]),
|
||||
str(row["name"]),
|
||||
str(row["parent"]),
|
||||
source_refs_text(row["source_refs"]),
|
||||
]
|
||||
).lower()
|
||||
if query and query not in haystack:
|
||||
continue
|
||||
filtered.append(row)
|
||||
return filtered
|
||||
|
||||
|
||||
def render_element_rows(rows: list[dict]) -> str:
|
||||
if not rows:
|
||||
return '<tr><td colspan="4" class="muted">No matching elements.</td></tr>'
|
||||
return "\n".join(render_element_row(row) for row in rows)
|
||||
|
||||
|
||||
def render_element_row(row: dict) -> str:
|
||||
return f"""
|
||||
<tr>
|
||||
<td>{escape(item_type)}</td>
|
||||
<td>{escape(name)}</td>
|
||||
<td>{escape(parent)}</td>
|
||||
<td>{render_sources(source_refs)}</td>
|
||||
<td><span class="pill">{escape(str(row["primary_class"]))}</span></td>
|
||||
<td>{escape(str(row["name"]))}</td>
|
||||
<td>{escape(str(row["parent"]))}</td>
|
||||
<td>{render_sources(row["source_refs"])}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
|
||||
def render_class_datalist(rows: list[dict]) -> str:
|
||||
classes = sorted({str(row["primary_class"]) for row in rows if row["primary_class"]})
|
||||
options = "".join(
|
||||
f'<option value="{escape(item)}"></option>'
|
||||
for item in classes
|
||||
)
|
||||
return f'<datalist id="element-classes">{options}</datalist>'
|
||||
|
||||
|
||||
def render_optional_hidden(name: str, value: int | None) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
return f'<input type="hidden" name="{escape(name)}" value="{value}">'
|
||||
|
||||
|
||||
def render_analysis_run_query_suffix(analysis_run_id: int | None) -> str:
|
||||
if analysis_run_id is None:
|
||||
return ""
|
||||
return f"&analysis_run_id={analysis_run_id}"
|
||||
|
||||
|
||||
def source_refs_text(source_refs: list[dict]) -> str:
|
||||
return " ".join(
|
||||
" ".join(
|
||||
str(ref.get(key, ""))
|
||||
for key in ("kind", "name", "path", "line")
|
||||
)
|
||||
for ref in source_refs
|
||||
)
|
||||
|
||||
|
||||
def render_candidate_graph(graph: dict, repository_id: int, analysis_run_id: int) -> str:
|
||||
abilities = graph.get("abilities", [])
|
||||
if not abilities:
|
||||
|
||||
Reference in New Issue
Block a user