Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 39 additions & 12 deletions core/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -727,23 +727,37 @@ def _paging(**params):

return _paging(RequestFactory().get("/", params))

def test_defaults(self):
from core.views.tables import DEFAULT_PAGE_SIZE
def test_default_is_fit_to_window(self):
# 既定は「画面に入るぶんだけ」。JS が測る前の 1 回目は FIT_FALLBACK。
from core.views.tables import FIT_FALLBACK

self.assertEqual(self._paging(), (DEFAULT_PAGE_SIZE, 0))
self.assertEqual(self._paging(), (FIT_FALLBACK, 0))

def test_offset_follows_the_page(self):
def test_fit_takes_the_measured_number(self):
self.assertEqual(self._paging(rows="fit", fit="27"), (27, 0))

def test_fit_is_clamped(self):
# **ブラウザから来た数をそのまま LIMIT に渡さない。**
from core.views.tables import FIT_MAX, FIT_MIN

self.assertEqual(self._paging(rows="fit", fit="99999")[0], FIT_MAX)
self.assertEqual(self._paging(rows="fit", fit="0")[0], FIT_MIN)
self.assertEqual(self._paging(rows="fit", fit="-5")[0], FIT_MIN)

def test_explicit_size_still_works(self):
self.assertEqual(self._paging(page="3", rows="100"), (100, 200))

def test_junk_falls_back_quietly(self):
# ここでエラーを出しても、利用者にできることが無い。
from core.views.tables import DEFAULT_PAGE_SIZE
from core.views.tables import FIT_FALLBACK, FIT_MAX, FIT_MIN

allowed = {FIT_FALLBACK, 100, 500, 1000, 5000}
for params in [{"page": "abc"}, {"page": "-5"}, {"rows": "99999"},
{"rows": "0"}, {"rows": "; DROP TABLE x"}]:
{"rows": "0"}, {"rows": "; DROP TABLE x"},
{"rows": "fit", "fit": "abc"}, {"rows": "fit", "fit": ""}]:
with self.subTest(params=params):
size, offset = self._paging(**params)
self.assertIn(size, (DEFAULT_PAGE_SIZE, 100, 500, 1000, 5000))
self.assertTrue(size in allowed or FIT_MIN <= size <= FIT_MAX)
self.assertGreaterEqual(offset, 0)


Expand Down Expand Up @@ -3495,11 +3509,22 @@ def setUp(self):
def tearDown(self):
self.page.close()

def open_section(self, name):
"""Navigate to a section through the overview hover menu (the bento)."""
self.page.get_by_role("button", name="overview").hover()
self.page.locator("#nav-menu").get_by_role("button", name=name).click()
# どの区域がどのメニューに入っているか。2026-09-05 に 1 つのホバー
# メガメニューからグループごとのクリックメニューへ変えた。
NAV_GROUPS = {
"query": ("SQL runner", "Index lab", "Snapshots", "History"),
"ops": ("Activity", "Locks", "Health", "Replication"),
"catalog": ("Objects", "Dependencies", "Extensions", "Backups", "Config"),
}

def open_section(self, name):
"""Navigate to a section through its group menu in the top bar."""
key = next((k for k, v in self.NAV_GROUPS.items() if name in v), None)
self.assertIsNotNone(key, f"no nav group holds {name}")
self.page.locator(f'button[data-navgroup="{key}"]').click()
panel = self.page.locator(f"#nav-menu-{key}")
panel.wait_for(state="visible")
panel.get_by_role("button", name=name).click()

@unittest.skipUnless(_HAS_PLAYWRIGHT and _sampledb_reachable(),
"needs playwright + chromium and a reachable sample DB")
Expand Down Expand Up @@ -3722,7 +3747,9 @@ def test_rename_then_drop_table(self):
page.locator(f'button[hx-get$="table={self.TBL}"]').click()
expect(page.locator("#detail h2")).to_have_text(f"public.{self.TBL}")

# Rename it via the header drawer; both panes update from one response.
# Rename it via the drawer; both panes update from one response.
# rename は底の帯の ⋯ の中(帯を 1 行に収めるため 2026-09-05 に移した)。
page.locator("#detail").get_by_role("button", name="⋯").click()
page.locator("#detail").get_by_role("button", name="rename", exact=True).click()
form = page.locator('form[hx-post*="table/rename"]')
form.locator("input[name=new_name]").fill(self.TBL2)
Expand Down
50 changes: 40 additions & 10 deletions core/views/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,42 @@
# 1 ページに出す行数。既定は 1000 ── テーブル名を押した人が見たいのは中身で、
# 50 行では「入っているものを見た」ことにならない。選べる値は固定の allow-list に
# する(画面から来た数をそのまま LIMIT に渡さないため)。
# 選べる 1 ページの大きさ。`fit` は「画面に入るぶんだけ」で、これが既定。
#
# 1000 行を出しても、画面に見えるのは 16 行ほどで、残り 98% はスクロールしないと
# 見えない。**1000 行をスクロールして目で探す人はいない** ── 探すなら絞り込みか
# SQL を使う。だから既定は「ちょうど収まるだけ」にして、スクロールバーを消す。
# たくさん要る人(コピーして持っていくなど)は選べる。
PAGE_SIZES = (100, 500, 1000, 5000)
DEFAULT_PAGE_SIZE = 1000
DEFAULT_PAGE_SIZE = "fit"

# `fit` のときに受け付ける範囲。画面の高さから割り出した数がブラウザから来るので、
# **そのまま LIMIT には渡さない。**
FIT_MIN, FIT_MAX = 10, 200
FIT_FALLBACK = 40 # JS が動く前の 1 回目。だいたい 1400×900 で埋まる数


def _paging(request):
"""`?page=` と `?rows=` を、範囲の決まった (page_size, offset) に直す。

壊れた値・負の値・一覧に無い大きさは、黙って既定へ落とす ── ここで
エラーを出しても、利用者にできることが無い。
`rows=fit` は画面に合わせる指定で、実際の行数は `?fit=` で届く。壊れた値・
負の値・一覧に無い大きさは、黙って既定へ落とす ── ここでエラーを出しても、
利用者にできることが無い。
"""
try:
size = int(request.GET.get("rows", DEFAULT_PAGE_SIZE))
except (TypeError, ValueError):
size = DEFAULT_PAGE_SIZE
if size not in PAGE_SIZES:
size = DEFAULT_PAGE_SIZE
raw = request.GET.get("rows", DEFAULT_PAGE_SIZE)
if raw == "fit" or raw is None:
try:
size = int(request.GET.get("fit", FIT_FALLBACK))
except (TypeError, ValueError):
size = FIT_FALLBACK
size = min(FIT_MAX, max(FIT_MIN, size))
else:
try:
size = int(raw)
except (TypeError, ValueError):
size = FIT_FALLBACK
if size not in PAGE_SIZES:
size = FIT_FALLBACK
try:
page = max(1, int(request.GET.get("page", 1)))
except (TypeError, ValueError):
Expand Down Expand Up @@ -92,7 +112,7 @@ def _write_previews(engine, schema, table):


def _render_detail(request, connection, schema, table, error=None, notice=None,
page_size=DEFAULT_PAGE_SIZE, offset=0):
page_size=FIT_FALLBACK, offset=0):
"""Render the table-detail panel: columns, indexes and a row preview.

A connection-level failure falls back to the error partial; a per-action
Expand Down Expand Up @@ -132,9 +152,19 @@ def _render_detail(request, connection, schema, table, error=None, notice=None,
"index_methods": INDEX_METHODS,
"column_types": COLUMN_TYPES,
"preview_columns": preview.columns,
# 列名だけでなく型も見出しに出す(Sequel Ace と同じ)。型は
# Columns タブを開かないと分からない、という往復をなくす。
"preview_header": [
(name, next((c.type for c in columns if c.name == name), ""))
for name in preview.columns
],
"preview_rows": rows,
"preview": preview,
"page_sizes": PAGE_SIZES,
# いま「画面に合わせる」で見ているか(選択欄の既定を出し分ける)。
"fit_mode": request.GET.get("rows", DEFAULT_PAGE_SIZE) == "fit",
# ページ送りのリンクが持ち回る指定。fit なら fit のまま送る。
"mode": "fit" if request.GET.get("rows", DEFAULT_PAGE_SIZE) == "fit" else preview.page_size,
# Display-only: prefilled into the SQL editor as a starting point,
# never executed server-side. The run path (run_query) is read-only
# enforced by the DB. nosec B608 — not a query we build and execute.
Expand Down
Binary file modified locale/ja/LC_MESSAGES/django.mo
Binary file not shown.
Loading
Loading