Split the model admin into a list and a page per model
/admin/models rendered a full edit form for every model. With eight that
was merely long; with a hundred it was unusable, which is the report.
The list is now compact rows only -- avatar, name, badges, position,
reorder buttons, Edit link -- with search across id and display name,
filter tabs (All / Enabled / Disabled / Pinned / Restricted, each with a
count), a connection filter, and pagination at 40. Filters are links, so
a filtered view is a real URL you can keep. Editing moved to
/admin/models/{id}/edit, one model per page, with Previous/Next links so
a freshly imported connection can be tidied without returning to the
list each time.
Measured with 128 models: the list is 73 KB showing 40 rows over 4
pages, and a detail page is 17 KB. The old page would have rendered all
128 forms into one response.
Reordering needed rethinking at that size too. Up/down is fine for
nudging a model one place but hopeless for moving it sixty, so the
detail page has a position field you type into; the value is clamped and
a non-numeric one is ignored rather than throwing. The move buttons take
a `back` field so they return to whatever filtered, paginated view they
were pressed on instead of dumping you at page 1.
Also adds a select-all checkbox for the bulk bar, scoped to a container
selector rather than the page so a future list can carry more than one.
212 tests, ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -477,3 +477,186 @@ def test_bulk_with_nothing_selected_is_harmless(client: TestClient, db, register
|
||||
assert client.post(
|
||||
"/admin/models/bulk", data={"action": "disable"}, follow_redirects=False
|
||||
).status_code == 303
|
||||
|
||||
|
||||
# --- The model admin list ----------------------------------------------------
|
||||
def _many_models(db, count: int) -> None:
|
||||
connection = _connection(db)
|
||||
db.add_all(
|
||||
[
|
||||
Model(connection_id=connection.id, model_id=f"model-{i:03d}", position=i)
|
||||
for i in range(count)
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
def test_the_list_paginates_rather_than_rendering_everything(
|
||||
client: TestClient, db, registered
|
||||
):
|
||||
"""A hundred models must not become a hundred forms on one page."""
|
||||
from lembas.api.admin_models import PAGE_SIZE
|
||||
|
||||
_many_models(db, PAGE_SIZE + 15)
|
||||
page = client.get("/admin/models").text
|
||||
|
||||
assert page.count('name="model_ids"') == PAGE_SIZE
|
||||
assert f"of {PAGE_SIZE + 15}" in page
|
||||
assert "Page 1 of 2" in page
|
||||
|
||||
|
||||
def test_the_second_page_shows_the_remainder(client: TestClient, db, registered):
|
||||
from lembas.api.admin_models import PAGE_SIZE
|
||||
|
||||
_many_models(db, PAGE_SIZE + 15)
|
||||
page = client.get("/admin/models?page=2").text
|
||||
assert page.count('name="model_ids"') == 15
|
||||
|
||||
|
||||
def test_an_out_of_range_page_is_clamped(client: TestClient, db, registered):
|
||||
_many_models(db, 5)
|
||||
assert "Page 1 of 1" not in client.get("/admin/models?page=99").text
|
||||
assert client.get("/admin/models?page=99").status_code == 200
|
||||
|
||||
|
||||
def test_the_list_does_not_render_edit_forms(client: TestClient, db, registered):
|
||||
"""The whole point of the split: rows link to a page, they are not forms."""
|
||||
_many_models(db, 3)
|
||||
page = client.get("/admin/models").text
|
||||
assert 'name="system_prompt"' not in page
|
||||
assert 'name="display_name"' not in page
|
||||
assert page.count("/edit") >= 3
|
||||
|
||||
|
||||
def test_search_narrows_the_list(client: TestClient, db, registered):
|
||||
connection = _connection(db)
|
||||
db.add_all(
|
||||
[
|
||||
Model(connection_id=connection.id, model_id="llama-large", position=0),
|
||||
Model(connection_id=connection.id, model_id="qwen-small", position=1),
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
|
||||
page = client.get("/admin/models?q=qwen").text
|
||||
assert "qwen-small" in page
|
||||
assert "llama-large" not in page
|
||||
|
||||
|
||||
def test_search_matches_the_display_name_too(client: TestClient, db, registered):
|
||||
connection = _connection(db)
|
||||
db.add(
|
||||
Model(connection_id=connection.id, model_id="abc-123", display_name="Friendly Name")
|
||||
)
|
||||
db.commit()
|
||||
assert "abc-123" in client.get("/admin/models?q=friendly").text
|
||||
|
||||
|
||||
def test_filter_tabs_narrow_the_list(client: TestClient, db, registered):
|
||||
connection = _connection(db)
|
||||
db.add_all(
|
||||
[
|
||||
Model(connection_id=connection.id, model_id="on-model", position=0),
|
||||
Model(connection_id=connection.id, model_id="off-model", position=1, enabled=False),
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
|
||||
disabled = client.get("/admin/models?filter=disabled").text
|
||||
assert "off-model" in disabled
|
||||
assert ">on-model" not in disabled
|
||||
|
||||
|
||||
def test_filtering_by_connection(client: TestClient, db, registered):
|
||||
first = _connection(db)
|
||||
second = _connection(db)
|
||||
db.add_all(
|
||||
[
|
||||
Model(connection_id=first.id, model_id="from-first", position=0),
|
||||
Model(connection_id=second.id, model_id="from-second", position=1),
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
|
||||
page = client.get(f"/admin/models?connection={second.id}").text
|
||||
assert "from-second" in page
|
||||
assert "from-first" not in page
|
||||
|
||||
|
||||
# --- The model detail page ---------------------------------------------------
|
||||
def test_the_detail_page_carries_the_full_form(client: TestClient, db, registered):
|
||||
_many_models(db, 3)
|
||||
db.add(Group(name="Insiders"))
|
||||
db.commit()
|
||||
model = db.scalar(select(Model).where(Model.model_id == "model-001"))
|
||||
|
||||
page = client.get(f"/admin/models/{model.id}/edit").text
|
||||
for field in ('name="display_name"', 'name="system_prompt"', 'name="capability"',
|
||||
'name="group_ids"', 'name="position"'):
|
||||
assert field in page, field
|
||||
|
||||
|
||||
def test_the_detail_page_links_to_its_neighbours(client: TestClient, db, registered):
|
||||
_many_models(db, 3)
|
||||
first, middle, last = db.scalars(select(Model).order_by(Model.position)).all()
|
||||
|
||||
page = client.get(f"/admin/models/{middle.id}/edit").text
|
||||
assert f"/admin/models/{first.id}/edit" in page
|
||||
assert f"/admin/models/{last.id}/edit" in page
|
||||
assert "2 of 3" in page
|
||||
|
||||
|
||||
def test_an_unknown_model_detail_is_404(client: TestClient, db, registered):
|
||||
assert client.get("/admin/models/nope/edit").status_code == 404
|
||||
|
||||
|
||||
def test_saving_from_the_detail_page_returns_to_it(client: TestClient, db, registered):
|
||||
_many_models(db, 2)
|
||||
model = db.scalar(select(Model).where(Model.model_id == "model-000"))
|
||||
|
||||
response = client.post(
|
||||
f"/admin/models/{model.id}",
|
||||
data={"display_name": "Renamed", "enabled": "true", "public": "true"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert response.headers["location"].startswith(f"/admin/models/{model.id}/edit")
|
||||
|
||||
db.refresh(model)
|
||||
assert model.display_name == "Renamed"
|
||||
|
||||
|
||||
def test_typing_a_position_moves_the_model(client: TestClient, db, registered):
|
||||
"""Up/down is unusable for moving a model 60 places."""
|
||||
_many_models(db, 5)
|
||||
last = db.scalar(select(Model).where(Model.model_id == "model-004"))
|
||||
|
||||
client.post(
|
||||
f"/admin/models/{last.id}",
|
||||
data={"display_name": "", "enabled": "true", "public": "true", "position": "1"},
|
||||
)
|
||||
order = [m.model_id for m in db.scalars(select(Model).order_by(Model.position))]
|
||||
assert order[0] == "model-004"
|
||||
|
||||
|
||||
def test_a_nonsense_position_is_ignored(client: TestClient, db, registered):
|
||||
_many_models(db, 3)
|
||||
model = db.scalar(select(Model).where(Model.model_id == "model-000"))
|
||||
|
||||
client.post(
|
||||
f"/admin/models/{model.id}",
|
||||
data={"display_name": "", "enabled": "true", "public": "true", "position": "abc"},
|
||||
)
|
||||
db.refresh(model)
|
||||
assert model.position == 0
|
||||
|
||||
|
||||
def test_moving_returns_to_the_filtered_view(client: TestClient, db, registered):
|
||||
_many_models(db, 3)
|
||||
model = db.scalar(select(Model).where(Model.model_id == "model-001"))
|
||||
|
||||
response = client.post(
|
||||
f"/admin/models/{model.id}/move",
|
||||
data={"direction": "up", "back": "/admin/models?filter=enabled&page=2"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert response.headers["location"] == "/admin/models?filter=enabled&page=2"
|
||||
|
||||
Reference in New Issue
Block a user