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 dataclasses import asdict
|
||||||
from html import escape
|
from html import escape
|
||||||
from pathlib import Path
|
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 import APIRouter, Depends, Form, HTTPException, Query
|
||||||
from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse
|
||||||
@@ -17,6 +17,25 @@ router = APIRouter(include_in_schema=False)
|
|||||||
APP_NAME = "Repository Scoping"
|
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(
|
def page(
|
||||||
title: str,
|
title: str,
|
||||||
body: str,
|
body: str,
|
||||||
@@ -238,7 +257,7 @@ def render_repository_index(
|
|||||||
rows = "\n".join(
|
rows = "\n".join(
|
||||||
f"""
|
f"""
|
||||||
<tr>
|
<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><span class="pill">{escape(repo.status)}</span></td>
|
||||||
<td class="source">{escape(repo.branch)}</td>
|
<td class="source">{escape(repo.branch)}</td>
|
||||||
<td class="source">{escape(repo.url)}</td>
|
<td class="source">{escape(repo.url)}</td>
|
||||||
@@ -260,6 +279,15 @@ def render_repository_index(
|
|||||||
<h1>Repositories</h1>
|
<h1>Repositories</h1>
|
||||||
{error}
|
{error}
|
||||||
<div class="grid">
|
<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">
|
<section class="panel">
|
||||||
<h2>Register Repository</h2>
|
<h2>Register Repository</h2>
|
||||||
<form class="stack" method="post" action="/ui/repos">
|
<form class="stack" method="post" action="/ui/repos">
|
||||||
@@ -276,16 +304,6 @@ def render_repository_index(
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</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>
|
</div>
|
||||||
"""
|
"""
|
||||||
response = page("Repositories", body)
|
response = page("Repositories", body)
|
||||||
@@ -323,6 +341,7 @@ def repository_scope_document(
|
|||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
repository = service.get_repository(repository_id)
|
repository = service.get_repository(repository_id)
|
||||||
|
display_name = repository_display_name(repository)
|
||||||
checkout = service.ingestion.cached_checkout(repository.url)
|
checkout = service.ingestion.cached_checkout(repository.url)
|
||||||
if checkout is None:
|
if checkout is None:
|
||||||
rendered = (
|
rendered = (
|
||||||
@@ -342,14 +361,14 @@ def repository_scope_document(
|
|||||||
body = f"""
|
body = f"""
|
||||||
<h1>SCOPE.md</h1>
|
<h1>SCOPE.md</h1>
|
||||||
<section class="panel stack">
|
<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}
|
{rendered}
|
||||||
</section>
|
</section>
|
||||||
"""
|
"""
|
||||||
return page(
|
return page(
|
||||||
"SCOPE.md",
|
"SCOPE.md",
|
||||||
body,
|
body,
|
||||||
selected_repository=repository.name,
|
selected_repository=display_name,
|
||||||
selected_repository_id=repository.id,
|
selected_repository_id=repository.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -721,6 +740,7 @@ def repository_detail(
|
|||||||
repository = service.get_repository(repository_id)
|
repository = service.get_repository(repository_id)
|
||||||
except NotFoundError as exc:
|
except NotFoundError as exc:
|
||||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
display_name = repository_display_name(repository)
|
||||||
runs = service.list_analysis_runs(repository_id)
|
runs = service.list_analysis_runs(repository_id)
|
||||||
ability_map = service.ability_map(repository_id)
|
ability_map = service.ability_map(repository_id)
|
||||||
latest_candidate = latest_completed_candidate_graph(
|
latest_candidate = latest_completed_candidate_graph(
|
||||||
@@ -746,7 +766,7 @@ def repository_detail(
|
|||||||
)
|
)
|
||||||
body = f"""
|
body = f"""
|
||||||
<div class="actions">
|
<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}/export">Export</a>
|
||||||
<a class="button secondary" href="/ui/repos/{repository_id}/scope">SCOPE</a>
|
<a class="button secondary" href="/ui/repos/{repository_id}/scope">SCOPE</a>
|
||||||
<a class="button secondary" href="/ui">Back</a>
|
<a class="button secondary" href="/ui">Back</a>
|
||||||
@@ -889,9 +909,9 @@ def repository_detail(
|
|||||||
</section>
|
</section>
|
||||||
"""
|
"""
|
||||||
return page(
|
return page(
|
||||||
repository.name,
|
display_name,
|
||||||
body,
|
body,
|
||||||
selected_repository=repository.name,
|
selected_repository=display_name,
|
||||||
selected_repository_id=repository.id,
|
selected_repository_id=repository.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1254,6 +1274,7 @@ def analysis_run_detail(
|
|||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
repository = service.get_repository(repository_id)
|
repository = service.get_repository(repository_id)
|
||||||
|
display_name = repository_display_name(repository)
|
||||||
candidate_graph = service.candidate_graph(repository_id, analysis_run_id)
|
candidate_graph = service.candidate_graph(repository_id, analysis_run_id)
|
||||||
candidate_graph_data = asdict(candidate_graph)
|
candidate_graph_data = asdict(candidate_graph)
|
||||||
facts = service.list_observed_facts(repository_id, analysis_run_id)
|
facts = service.list_observed_facts(repository_id, analysis_run_id)
|
||||||
@@ -1273,7 +1294,7 @@ def analysis_run_detail(
|
|||||||
)
|
)
|
||||||
body = f"""
|
body = f"""
|
||||||
<div class="actions">
|
<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))}
|
{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>
|
<a class="button secondary" href="/ui/repos/{repository_id}">Repository</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -1346,9 +1367,9 @@ def analysis_run_detail(
|
|||||||
</section>
|
</section>
|
||||||
"""
|
"""
|
||||||
return page(
|
return page(
|
||||||
f"{repository.name} Run {analysis_run_id}",
|
f"{display_name} Run {analysis_run_id}",
|
||||||
body,
|
body,
|
||||||
selected_repository=repository.name,
|
selected_repository=display_name,
|
||||||
selected_repository_id=repository.id,
|
selected_repository_id=repository.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1411,9 +1432,10 @@ def repository_element_listing(
|
|||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
repository = service.get_repository(repository_id)
|
repository = service.get_repository(repository_id)
|
||||||
|
display_name = repository_display_name(repository)
|
||||||
if scope in {"approved", "candidate"} and not entry_filter:
|
if scope in {"approved", "candidate"} and not entry_filter:
|
||||||
entry_filter = scope
|
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"}:
|
if scope in {"all", "approved", "candidate"}:
|
||||||
elements = graph_element_rows(
|
elements = graph_element_rows(
|
||||||
asdict(service.ability_map(repository_id)),
|
asdict(service.ability_map(repository_id)),
|
||||||
@@ -1519,7 +1541,7 @@ def repository_element_listing(
|
|||||||
return page(
|
return page(
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
selected_repository=repository.name,
|
selected_repository=display_name,
|
||||||
selected_repository_id=repository.id,
|
selected_repository_id=repository.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1539,9 +1561,10 @@ def analysis_run_diff_detail(
|
|||||||
)
|
)
|
||||||
except NotFoundError as exc:
|
except NotFoundError as exc:
|
||||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
display_name = repository_display_name(diff.repository)
|
||||||
body = f"""
|
body = f"""
|
||||||
<div class="actions">
|
<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}/analysis-runs/{target_analysis_run_id}">Target Run</a>
|
||||||
<a class="button secondary" href="/ui/repos/{repository_id}">Repository</a>
|
<a class="button secondary" href="/ui/repos/{repository_id}">Repository</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -1575,9 +1598,9 @@ def analysis_run_diff_detail(
|
|||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
return page(
|
return page(
|
||||||
f"{diff.repository.name} Change Review",
|
f"{display_name} Change Review",
|
||||||
body,
|
body,
|
||||||
selected_repository=diff.repository.name,
|
selected_repository=display_name,
|
||||||
selected_repository_id=diff.repository.id,
|
selected_repository_id=diff.repository.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2147,7 +2170,7 @@ def render_repository_checkbox_list(
|
|||||||
<label>
|
<label>
|
||||||
<span>
|
<span>
|
||||||
<input style="width:auto" type="checkbox" name="repository_ids" value="{repository.id}"{disabled}>
|
<input style="width:auto" type="checkbox" name="repository_ids" value="{repository.id}"{disabled}>
|
||||||
{escape(repository.name)}
|
{escape(repository_display_name(repository))}
|
||||||
</span>
|
</span>
|
||||||
<span class="muted">{escape(status)}</span>
|
<span class="muted">{escape(status)}</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -2162,7 +2185,7 @@ def render_compared_repositories(repositories: list[dict]) -> str:
|
|||||||
rows = "\n".join(
|
rows = "\n".join(
|
||||||
f"""
|
f"""
|
||||||
<tr>
|
<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><span class="pill">{escape(repository['status'])}</span></td>
|
||||||
<td class="source">{escape(repository['branch'])}</td>
|
<td class="source">{escape(repository['branch'])}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -323,6 +323,43 @@ def test_ui_uses_repository_scoping_brand():
|
|||||||
assert "Repository Ability Registry" not in response.text
|
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():
|
def test_ui_scope_page_presents_scope_md():
|
||||||
client = TestClient(app)
|
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")
|
repo_scope_response = client.get(f"/ui/repos/{repository_id}/scope")
|
||||||
assert repo_scope_response.status_code == 200
|
assert repo_scope_response.status_code == 200
|
||||||
assert (
|
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
|
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
|
assert "UI Repo owns the status reporting scope." in repo_scope_response.text
|
||||||
|
|
||||||
edit_repository_response = client.post(
|
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"])
|
approved_detail = client.get(approve_response.headers["location"])
|
||||||
assert approved_detail.status_code == 200
|
assert approved_detail.status_code == 200
|
||||||
assert (
|
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
|
in approved_detail.text
|
||||||
)
|
)
|
||||||
assert "Approved Characteristics" 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 scope_listing.status_code == 200
|
||||||
assert (
|
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
|
in scope_listing.text
|
||||||
)
|
)
|
||||||
assert f'<a href="/ui/repos/{repository_id}">UI 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 discovery.status_code == 200
|
||||||
assert "Compare Repositories" in discovery.text
|
assert "Compare Repositories" in discovery.text
|
||||||
assert "Capability Gap Report" 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
|
assert "No approved profile" in discovery.text
|
||||||
|
|
||||||
comparison = client.get(
|
comparison = client.get(
|
||||||
|
|||||||
Reference in New Issue
Block a user