generated from coulomb/repo-seed
Layout optimization initial page
This commit is contained in:
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from dataclasses import asdict
|
||||
from html import escape
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
from urllib.parse import quote_plus, urlparse
|
||||
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Query
|
||||
from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse
|
||||
@@ -17,6 +17,25 @@ router = APIRouter(include_in_schema=False)
|
||||
APP_NAME = "Repository Scoping"
|
||||
|
||||
|
||||
def repository_directory_name(url: str, fallback: str) -> str:
|
||||
parsed = urlparse(url)
|
||||
path = parsed.path if parsed.scheme or parsed.netloc else url
|
||||
normalized = path.replace("\\", "/").rstrip("/")
|
||||
name = normalized.rsplit("/", 1)[-1] if normalized else ""
|
||||
if name.endswith(".git"):
|
||||
name = name[:-4]
|
||||
return name or fallback
|
||||
|
||||
|
||||
def repository_display_name(repository) -> str:
|
||||
return repository_directory_name(repository.url, repository.name)
|
||||
|
||||
|
||||
def repository_dict_display_name(repository: dict) -> str:
|
||||
fallback = str(repository.get("name") or repository.get("repository_name") or "")
|
||||
return repository_directory_name(str(repository.get("url") or ""), fallback)
|
||||
|
||||
|
||||
def page(
|
||||
title: str,
|
||||
body: str,
|
||||
@@ -238,7 +257,7 @@ def render_repository_index(
|
||||
rows = "\n".join(
|
||||
f"""
|
||||
<tr>
|
||||
<td><a href="/ui/repos/{repo.id}">{escape(repo.name)}</a></td>
|
||||
<td><a href="/ui/repos/{repo.id}">{escape(repository_display_name(repo))}</a></td>
|
||||
<td><span class="pill">{escape(repo.status)}</span></td>
|
||||
<td class="source">{escape(repo.branch)}</td>
|
||||
<td class="source">{escape(repo.url)}</td>
|
||||
@@ -260,6 +279,15 @@ def render_repository_index(
|
||||
<h1>Repositories</h1>
|
||||
{error}
|
||||
<div class="grid">
|
||||
<section class="panel">
|
||||
<div class="actions">
|
||||
<h2 style="margin-right:auto">Registry</h2>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Status</th><th>Branch</th><th>Source</th></tr></thead>
|
||||
<tbody>{rows or '<tr><td colspan="4" class="muted">No repositories yet.</td></tr>'}</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<h2>Register Repository</h2>
|
||||
<form class="stack" method="post" action="/ui/repos">
|
||||
@@ -276,16 +304,6 @@ def render_repository_index(
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<div class="actions">
|
||||
<h2 style="margin-right:auto">Registry</h2>
|
||||
<a class="button secondary" href="/ui/discovery">Discovery</a>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Status</th><th>Branch</th><th>Source</th></tr></thead>
|
||||
<tbody>{rows or '<tr><td colspan="4" class="muted">No repositories yet.</td></tr>'}</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
"""
|
||||
response = page("Repositories", body)
|
||||
@@ -323,6 +341,7 @@ def repository_scope_document(
|
||||
service: RegistryService = Depends(get_service),
|
||||
) -> HTMLResponse:
|
||||
repository = service.get_repository(repository_id)
|
||||
display_name = repository_display_name(repository)
|
||||
checkout = service.ingestion.cached_checkout(repository.url)
|
||||
if checkout is None:
|
||||
rendered = (
|
||||
@@ -342,14 +361,14 @@ def repository_scope_document(
|
||||
body = f"""
|
||||
<h1>SCOPE.md</h1>
|
||||
<section class="panel stack">
|
||||
<p class="muted">Canonical scope summary for the {escape(repository.name)} repository.</p>
|
||||
<p class="muted">Canonical scope summary for the {escape(display_name)} repository.</p>
|
||||
{rendered}
|
||||
</section>
|
||||
"""
|
||||
return page(
|
||||
"SCOPE.md",
|
||||
body,
|
||||
selected_repository=repository.name,
|
||||
selected_repository=display_name,
|
||||
selected_repository_id=repository.id,
|
||||
)
|
||||
|
||||
@@ -721,6 +740,7 @@ def repository_detail(
|
||||
repository = service.get_repository(repository_id)
|
||||
except NotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
display_name = repository_display_name(repository)
|
||||
runs = service.list_analysis_runs(repository_id)
|
||||
ability_map = service.ability_map(repository_id)
|
||||
latest_candidate = latest_completed_candidate_graph(
|
||||
@@ -746,7 +766,7 @@ def repository_detail(
|
||||
)
|
||||
body = f"""
|
||||
<div class="actions">
|
||||
<h1 style="margin-right:auto">{escape(repository.name)}</h1>
|
||||
<h1 style="margin-right:auto">{escape(display_name)}</h1>
|
||||
<a class="button secondary" href="/ui/repos/{repository_id}/export">Export</a>
|
||||
<a class="button secondary" href="/ui/repos/{repository_id}/scope">SCOPE</a>
|
||||
<a class="button secondary" href="/ui">Back</a>
|
||||
@@ -889,9 +909,9 @@ def repository_detail(
|
||||
</section>
|
||||
"""
|
||||
return page(
|
||||
repository.name,
|
||||
display_name,
|
||||
body,
|
||||
selected_repository=repository.name,
|
||||
selected_repository=display_name,
|
||||
selected_repository_id=repository.id,
|
||||
)
|
||||
|
||||
@@ -1254,6 +1274,7 @@ def analysis_run_detail(
|
||||
service: RegistryService = Depends(get_service),
|
||||
) -> HTMLResponse:
|
||||
repository = service.get_repository(repository_id)
|
||||
display_name = repository_display_name(repository)
|
||||
candidate_graph = service.candidate_graph(repository_id, analysis_run_id)
|
||||
candidate_graph_data = asdict(candidate_graph)
|
||||
facts = service.list_observed_facts(repository_id, analysis_run_id)
|
||||
@@ -1273,7 +1294,7 @@ def analysis_run_detail(
|
||||
)
|
||||
body = f"""
|
||||
<div class="actions">
|
||||
<h1 style="margin-right:auto">{escape(repository.name)} · Run #{analysis_run_id}</h1>
|
||||
<h1 style="margin-right:auto">{escape(display_name)} · Run #{analysis_run_id}</h1>
|
||||
{render_run_detail_compare_link(repository_id, analysis_run_id, service.list_analysis_runs(repository_id))}
|
||||
<a class="button secondary" href="/ui/repos/{repository_id}">Repository</a>
|
||||
</div>
|
||||
@@ -1346,9 +1367,9 @@ def analysis_run_detail(
|
||||
</section>
|
||||
"""
|
||||
return page(
|
||||
f"{repository.name} Run {analysis_run_id}",
|
||||
f"{display_name} Run {analysis_run_id}",
|
||||
body,
|
||||
selected_repository=repository.name,
|
||||
selected_repository=display_name,
|
||||
selected_repository_id=repository.id,
|
||||
)
|
||||
|
||||
@@ -1411,9 +1432,10 @@ def repository_element_listing(
|
||||
service: RegistryService = Depends(get_service),
|
||||
) -> HTMLResponse:
|
||||
repository = service.get_repository(repository_id)
|
||||
display_name = repository_display_name(repository)
|
||||
if scope in {"approved", "candidate"} and not entry_filter:
|
||||
entry_filter = scope
|
||||
title = element_listing_title(repository.name, scope, type)
|
||||
title = element_listing_title(display_name, scope, type)
|
||||
if scope in {"all", "approved", "candidate"}:
|
||||
elements = graph_element_rows(
|
||||
asdict(service.ability_map(repository_id)),
|
||||
@@ -1519,7 +1541,7 @@ def repository_element_listing(
|
||||
return page(
|
||||
title,
|
||||
body,
|
||||
selected_repository=repository.name,
|
||||
selected_repository=display_name,
|
||||
selected_repository_id=repository.id,
|
||||
)
|
||||
|
||||
@@ -1539,9 +1561,10 @@ def analysis_run_diff_detail(
|
||||
)
|
||||
except NotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
display_name = repository_display_name(diff.repository)
|
||||
body = f"""
|
||||
<div class="actions">
|
||||
<h1 style="margin-right:auto">{escape(diff.repository.name)} · Change Review</h1>
|
||||
<h1 style="margin-right:auto">{escape(display_name)} · Change Review</h1>
|
||||
<a class="button secondary" href="/ui/repos/{repository_id}/analysis-runs/{target_analysis_run_id}">Target Run</a>
|
||||
<a class="button secondary" href="/ui/repos/{repository_id}">Repository</a>
|
||||
</div>
|
||||
@@ -1575,9 +1598,9 @@ def analysis_run_diff_detail(
|
||||
</div>
|
||||
"""
|
||||
return page(
|
||||
f"{diff.repository.name} Change Review",
|
||||
f"{display_name} Change Review",
|
||||
body,
|
||||
selected_repository=diff.repository.name,
|
||||
selected_repository=display_name,
|
||||
selected_repository_id=diff.repository.id,
|
||||
)
|
||||
|
||||
@@ -2147,7 +2170,7 @@ def render_repository_checkbox_list(
|
||||
<label>
|
||||
<span>
|
||||
<input style="width:auto" type="checkbox" name="repository_ids" value="{repository.id}"{disabled}>
|
||||
{escape(repository.name)}
|
||||
{escape(repository_display_name(repository))}
|
||||
</span>
|
||||
<span class="muted">{escape(status)}</span>
|
||||
</label>
|
||||
@@ -2162,7 +2185,7 @@ def render_compared_repositories(repositories: list[dict]) -> str:
|
||||
rows = "\n".join(
|
||||
f"""
|
||||
<tr>
|
||||
<td><a href="/ui/repos/{repository['id']}">{escape(repository['name'])}</a></td>
|
||||
<td><a href="/ui/repos/{repository['id']}">{escape(repository_dict_display_name(repository))}</a></td>
|
||||
<td><span class="pill">{escape(repository['status'])}</span></td>
|
||||
<td class="source">{escape(repository['branch'])}</td>
|
||||
</tr>
|
||||
|
||||
@@ -323,6 +323,43 @@ def test_ui_uses_repository_scoping_brand():
|
||||
assert "Repository Ability Registry" not in response.text
|
||||
|
||||
|
||||
def test_ui_homepage_registry_panel_uses_directory_identifier_and_left_column(tmp_path):
|
||||
source = tmp_path / "short-dir"
|
||||
source.mkdir()
|
||||
(source / "README.md").write_text("# Short Dir\n", encoding="utf-8")
|
||||
|
||||
def override_settings():
|
||||
return Settings(
|
||||
database_path=str(tmp_path / "ui-home.sqlite3"),
|
||||
checkout_root=str(tmp_path / "ui-home-checkouts"),
|
||||
)
|
||||
|
||||
app.dependency_overrides[get_settings] = override_settings
|
||||
client = TestClient(app)
|
||||
try:
|
||||
response = client.post(
|
||||
"/repos",
|
||||
json={
|
||||
"name": "Very Long Marketing Project Name",
|
||||
"url": str(source),
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
homepage = client.get("/ui")
|
||||
|
||||
assert homepage.status_code == 200
|
||||
assert homepage.text.index(">Registry<") < homepage.text.index(">Register Repository<")
|
||||
registry_panel = homepage.text[
|
||||
homepage.text.index(">Registry<") : homepage.text.index(">Register Repository<")
|
||||
]
|
||||
assert "short-dir" in registry_panel
|
||||
assert "Very Long Marketing Project Name" not in registry_panel
|
||||
assert "Discovery" not in registry_panel
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_ui_scope_page_presents_scope_md():
|
||||
client = TestClient(app)
|
||||
|
||||
@@ -1281,10 +1318,10 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
|
||||
repo_scope_response = client.get(f"/ui/repos/{repository_id}/scope")
|
||||
assert repo_scope_response.status_code == 200
|
||||
assert (
|
||||
f'<a class="header-context" href="/ui/repos/{repository_id}">UI Repo</a>'
|
||||
f'<a class="header-context" href="/ui/repos/{repository_id}">repo</a>'
|
||||
in repo_scope_response.text
|
||||
)
|
||||
assert "Canonical scope summary for the UI Repo repository." in repo_scope_response.text
|
||||
assert "Canonical scope summary for the repo repository." in repo_scope_response.text
|
||||
assert "UI Repo owns the status reporting scope." in repo_scope_response.text
|
||||
|
||||
edit_repository_response = client.post(
|
||||
@@ -1378,7 +1415,7 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
|
||||
approved_detail = client.get(approve_response.headers["location"])
|
||||
assert approved_detail.status_code == 200
|
||||
assert (
|
||||
f'<a class="header-context" href="/ui/repos/{repository_id}">UI Repo Edited</a>'
|
||||
f'<a class="header-context" href="/ui/repos/{repository_id}">repo</a>'
|
||||
in approved_detail.text
|
||||
)
|
||||
assert "Approved Characteristics" in approved_detail.text
|
||||
@@ -1409,7 +1446,7 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
|
||||
)
|
||||
assert scope_listing.status_code == 200
|
||||
assert (
|
||||
f'<a class="header-context" href="/ui/repos/{repository_id}">UI Repo Edited</a>'
|
||||
f'<a class="header-context" href="/ui/repos/{repository_id}">repo</a>'
|
||||
in scope_listing.text
|
||||
)
|
||||
assert f'<a href="/ui/repos/{repository_id}">UI Repo</a>' in scope_listing.text
|
||||
@@ -2362,7 +2399,7 @@ def test_ui_discovery_compare_gap_and_export(tmp_path):
|
||||
assert discovery.status_code == 200
|
||||
assert "Compare Repositories" in discovery.text
|
||||
assert "Capability Gap Report" in discovery.text
|
||||
assert "EmptyProfile" in discovery.text
|
||||
assert "empty-profile-ui" in discovery.text
|
||||
assert "No approved profile" in discovery.text
|
||||
|
||||
comparison = client.get(
|
||||
|
||||
Reference in New Issue
Block a user