diff --git a/core/tests.py b/core/tests.py index 1889ff8..398e299 100644 --- a/core/tests.py +++ b/core/tests.py @@ -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) @@ -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") @@ -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) diff --git a/core/views/tables.py b/core/views/tables.py index a976e04..a7972b6 100644 --- a/core/views/tables.py +++ b/core/views/tables.py @@ -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): @@ -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 @@ -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. diff --git a/locale/ja/LC_MESSAGES/django.mo b/locale/ja/LC_MESSAGES/django.mo index 1996873..f989f10 100644 Binary files a/locale/ja/LC_MESSAGES/django.mo and b/locale/ja/LC_MESSAGES/django.mo differ diff --git a/locale/ja/LC_MESSAGES/django.po b/locale/ja/LC_MESSAGES/django.po index 559593d..7630f94 100644 --- a/locale/ja/LC_MESSAGES/django.po +++ b/locale/ja/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-04 13:53+0000\n" +"POT-Creation-Date: 2026-09-05 03:26+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -93,7 +93,7 @@ msgid "Unsupported index method: %(method)s" msgstr "未対応のインデックス方式: %(method)s" #: core/engines/mysql.py:757 core/engines/postgres.py:178 -#: core/views/tables.py:223 +#: core/views/tables.py:247 msgid "Select at least one column to index." msgstr "インデックス対象の列を1つ以上選んでください。" @@ -316,24 +316,24 @@ msgstr "エクスポートする結果がありません — 先にクエリを msgid "No table specified." msgstr "テーブルが指定されていません。" -#: core/views/tables.py:183 +#: core/views/tables.py:207 msgid "Choose a CSV file to import." msgstr "インポートする CSV ファイルを選んでください。" -#: core/views/tables.py:191 +#: core/views/tables.py:215 #, python-format msgid "Imported %(n)s row(s) from %(name)s." msgstr "%(name)s から %(n)s 行をインポートしました。" -#: core/views/tables.py:257 +#: core/views/tables.py:281 msgid "Enter a new table name." msgstr "新しいテーブル名を入力してください。" -#: core/views/tables.py:308 +#: core/views/tables.py:332 msgid "Enter a column name." msgstr "列名を入力してください。" -#: core/views/tables.py:328 +#: core/views/tables.py:352 msgid "Enter a new column name." msgstr "新しい列名を入力してください。" @@ -420,7 +420,7 @@ msgid "— click columns in order" msgstr "— 列を順にクリック" #: planner_lab/templates/partials/index_lab.html:64 -#: templates/partials/detail.html:160 templates/partials/detail.html:499 +#: templates/partials/detail.html:165 templates/partials/detail.html:543 msgid "unique" msgstr "ユニーク" @@ -557,7 +557,7 @@ msgstr "" "スケールシミュレーションには superuser 接続が必要です(pg_class を編集するた" "め)。" -#: templates/base.html:155 +#: templates/base.html:246 msgid "CLI database ops, as buttons." msgstr "CLI のDB操作を、ボタンで。" @@ -569,15 +569,15 @@ msgstr "新しい接続" msgid "Defaults point at the bundled sample DB." msgstr "初期値は同梱のサンプルDBを指しています。" -#: templates/index.html:16 templates/partials/detail.html:79 -#: templates/partials/detail.html:381 templates/partials/detail.html:414 +#: templates/index.html:16 templates/partials/detail.html:84 +#: templates/partials/detail.html:425 templates/partials/detail.html:458 #: templates/partials/replication.html:172 msgid "Type" msgstr "種別" -#: templates/index.html:17 templates/partials/detail.html:78 -#: templates/partials/detail.html:147 templates/partials/detail.html:377 -#: templates/partials/detail.html:490 templates/partials/extensions.html:28 +#: templates/index.html:17 templates/partials/detail.html:83 +#: templates/partials/detail.html:152 templates/partials/detail.html:421 +#: templates/partials/detail.html:534 templates/partials/extensions.html:28 #: templates/partials/extensions.html:64 templates/partials/objects.html:34 #: templates/partials/objects.html:95 templates/partials/objects.html:184 #: templates/partials/objects.html:252 templates/partials/objects.html:263 @@ -586,8 +586,8 @@ msgstr "種別" msgid "Name" msgstr "名前" -#: templates/index.html:17 templates/partials/detail.html:387 -#: templates/partials/detail.html:490 templates/partials/objects.html:188 +#: templates/index.html:17 templates/partials/detail.html:431 +#: templates/partials/detail.html:534 templates/partials/objects.html:188 #: templates/partials/objects.html:283 templates/partials/objects.html:310 msgid "(optional)" msgstr "(任意)" @@ -889,7 +889,7 @@ msgstr "対象" msgid "Source DB" msgstr "元DB" -#: templates/partials/backups.html:17 templates/partials/detail.html:150 +#: templates/partials/backups.html:17 templates/partials/detail.html:155 #: templates/partials/health.html:69 templates/partials/health.html:169 #: templates/partials/health.html:270 templates/partials/health.html:423 #: templates/partials/objects.html:37 @@ -1018,8 +1018,8 @@ msgstr "子(参照する側)" msgid "Parent (referenced)" msgstr "親(参照される側)" -#: templates/partials/dependencies.html:88 templates/partials/detail.html:62 -#: templates/partials/detail.html:148 templates/partials/health.html:226 +#: templates/partials/dependencies.html:88 templates/partials/detail.html:67 +#: templates/partials/detail.html:153 templates/partials/health.html:226 #: templates/partials/health.html:316 msgid "Columns" msgstr "列" @@ -1028,93 +1028,93 @@ msgstr "列" msgid "Constraint" msgstr "制約" -#: templates/partials/detail.html:29 +#: templates/partials/detail.html:34 msgid "Open in SQL" msgstr "SQL で開く" -#: templates/partials/detail.html:33 +#: templates/partials/detail.html:38 msgid "backup" msgstr "バックアップ" -#: templates/partials/detail.html:35 +#: templates/partials/detail.html:40 msgid "pg_dump this table as plain SQL" msgstr "このテーブルを平文SQLで pg_dump" -#: templates/partials/detail.html:38 +#: templates/partials/detail.html:43 msgid "pg_dump this table in custom format" msgstr "このテーブルをカスタム形式で pg_dump" -#: templates/partials/detail.html:46 +#: templates/partials/detail.html:51 msgid "export" msgstr "エクスポート" -#: templates/partials/detail.html:48 +#: templates/partials/detail.html:53 msgid "Download every row as CSV" msgstr "全行を CSV でダウンロード" -#: templates/partials/detail.html:50 +#: templates/partials/detail.html:55 msgid "Download every row as JSON" msgstr "全行を JSON でダウンロード" -#: templates/partials/detail.html:53 +#: templates/partials/detail.html:58 msgid "Append rows from a CSV file" msgstr "CSV ファイルから行を追加" -#: templates/partials/detail.html:53 +#: templates/partials/detail.html:58 msgid "import" msgstr "インポート" -#: templates/partials/detail.html:54 templates/partials/objects.html:65 +#: templates/partials/detail.html:59 templates/partials/objects.html:65 msgid "rename" msgstr "リネーム" -#: templates/partials/detail.html:63 templates/partials/health.html:25 +#: templates/partials/detail.html:68 templates/partials/health.html:25 msgid "Indexes" msgstr "インデックス" -#: templates/partials/detail.html:64 +#: templates/partials/detail.html:69 msgid "Data" msgstr "データ" -#: templates/partials/detail.html:66 +#: templates/partials/detail.html:71 msgid "Danger" msgstr "危険" -#: templates/partials/detail.html:72 templates/partials/detail.html:391 +#: templates/partials/detail.html:77 templates/partials/detail.html:435 msgid "+ Add column" msgstr "+ 列を追加" -#: templates/partials/detail.html:80 templates/partials/health.html:114 +#: templates/partials/detail.html:85 templates/partials/health.html:114 msgid "Nullable" msgstr "NULL 許容" -#: templates/partials/detail.html:81 templates/partials/detail.html:387 -#: templates/partials/detail.html:458 +#: templates/partials/detail.html:86 templates/partials/detail.html:431 +#: templates/partials/detail.html:502 msgid "Default" msgstr "デフォルト" -#: templates/partials/detail.html:93 +#: templates/partials/detail.html:98 msgid "yes" msgstr "はい" -#: templates/partials/detail.html:93 +#: templates/partials/detail.html:98 msgid "no" msgstr "いいえ" -#: templates/partials/detail.html:96 +#: templates/partials/detail.html:101 msgid "generated" msgstr "生成列" -#: templates/partials/detail.html:108 +#: templates/partials/detail.html:113 msgid "shape" msgstr "構造" -#: templates/partials/detail.html:111 templates/partials/objects.html:108 +#: templates/partials/detail.html:116 templates/partials/objects.html:108 #: templates/partials/objects.html:152 msgid "edit" msgstr "編集" -#: templates/partials/detail.html:113 +#: templates/partials/detail.html:118 #, python-format msgid "" "Drop column "%(col)s" from %(schema)s.%(table)s? This permanently " @@ -1123,116 +1123,122 @@ msgstr "" "%(schema)s.%(table)s から列 "%(col)s" を削除しますか?データは完全" "に削除されます。" -#: templates/partials/detail.html:117 templates/partials/detail.html:175 +#: templates/partials/detail.html:122 templates/partials/detail.html:180 #: templates/partials/objects.html:69 templates/partials/objects.html:112 #: templates/partials/objects.html:156 templates/partials/replication.html:198 msgid "drop" msgstr "削除" -#: templates/partials/detail.html:127 +#: templates/partials/detail.html:132 msgid "Sampling…" msgstr "サンプリング中…" -#: templates/partials/detail.html:137 +#: templates/partials/detail.html:142 msgid "Try an index and measure it before creating it for real" msgstr "本番作成の前にインデックスを試して計測します" -#: templates/partials/detail.html:138 +#: templates/partials/detail.html:143 msgid "Try an index" msgstr "インデックスを試す" -#: templates/partials/detail.html:140 templates/partials/detail.html:500 +#: templates/partials/detail.html:145 templates/partials/detail.html:544 msgid "+ Create index" msgstr "+ インデックスを作成" -#: templates/partials/detail.html:149 templates/partials/detail.html:494 +#: templates/partials/detail.html:154 templates/partials/detail.html:538 msgid "Method" msgstr "方式" -#: templates/partials/detail.html:159 templates/partials/replication.html:26 +#: templates/partials/detail.html:164 templates/partials/replication.html:26 msgid "primary" msgstr "主キー" -#: templates/partials/detail.html:161 templates/partials/health.html:187 +#: templates/partials/detail.html:166 templates/partials/health.html:187 msgid "A concurrent build failed — this index is unusable; drop it." msgstr "並行ビルドに失敗 — このインデックスは使用不可です。削除してください。" -#: templates/partials/detail.html:161 +#: templates/partials/detail.html:166 msgid "invalid" msgstr "無効" -#: templates/partials/detail.html:168 +#: templates/partials/detail.html:173 msgid "pk" msgstr "pk" -#: templates/partials/detail.html:171 +#: templates/partials/detail.html:176 #, python-format msgid "Drop index "%(name)s"?" msgstr "インデックス "%(name)s" を削除しますか?" -#: templates/partials/detail.html:184 +#: templates/partials/detail.html:189 msgid "No indexes on this table yet." msgstr "このテーブルにはまだインデックスがありません。" -#: templates/partials/detail.html:199 +#: templates/partials/detail.html:203 +#, fuzzy +#| msgid "Filter error" +msgid "Filter" +msgstr "フィルタエラー" + +#: templates/partials/detail.html:218 msgid "column…" msgstr "列…" -#: templates/partials/detail.html:209 +#: templates/partials/detail.html:228 msgid "contains" msgstr "を含む" -#: templates/partials/detail.html:210 +#: templates/partials/detail.html:229 msgid "starts with" msgstr "で始まる" -#: templates/partials/detail.html:211 +#: templates/partials/detail.html:230 msgid "is null" msgstr "が NULL" -#: templates/partials/detail.html:212 +#: templates/partials/detail.html:231 msgid "is not null" msgstr "が NULL でない" -#: templates/partials/detail.html:215 +#: templates/partials/detail.html:234 msgid "value" msgstr "値" -#: templates/partials/detail.html:217 +#: templates/partials/detail.html:236 msgid "Remove condition" msgstr "条件を削除" -#: templates/partials/detail.html:221 +#: templates/partials/detail.html:240 msgid "+ Condition" msgstr "+ 条件" -#: templates/partials/detail.html:222 +#: templates/partials/detail.html:241 msgid "Apply filters" msgstr "フィルタを適用" -#: templates/partials/detail.html:223 +#: templates/partials/detail.html:242 msgid "All conditions are ANDed · read-only" msgstr "全条件は AND 結合・読み取り専用" -#: templates/partials/detail.html:232 +#: templates/partials/detail.html:262 #, fuzzy, python-format #| msgid "rows %(n)s" msgid "rows %(a)s–%(b)s" msgstr "行 %(n)s" -#: templates/partials/detail.html:234 +#: templates/partials/detail.html:264 #, fuzzy, python-format #| msgid "Rows: about %(n)s (estimated from statistics, last analyzed %(d)s)" msgid "of about %(n)s (estimated from statistics)" msgstr "行数: 約 %(n)s(統計からの推定・最終 ANALYZE %(d)s)" -#: templates/partials/detail.html:236 +#: templates/partials/detail.html:266 #, fuzzy #| msgid "Rows: unknown (this table has never been analyzed)" msgid "of an unknown total — this table has never been analyzed" msgstr "行数: 不明(このテーブルはまだ ANALYZE されていません)" -#: templates/partials/detail.html:241 +#: templates/partials/detail.html:271 msgid "" "A table with no primary key has no defined row order, so paging can repeat " "or skip rows." @@ -1240,48 +1246,52 @@ msgstr "" "主キーの無いテーブルには決まった行の順序がないので、ページをめくると同じ行が" "二度出たり、どこにも出ない行ができることがあります。" -#: templates/partials/detail.html:242 +#: templates/partials/detail.html:272 msgid "⚠ No primary key — row order is not guaranteed between pages" msgstr "⚠ 主キーが無いため、ページ間で行の順序は保証されません" -#: templates/partials/detail.html:249 +#: templates/partials/detail.html:279 msgid "← Prev" msgstr "← 前" -#: templates/partials/detail.html:253 +#: templates/partials/detail.html:283 #, python-format msgid "page %(p)s of about %(t)s" msgstr "%(p)s / 約 %(t)s ページ" -#: templates/partials/detail.html:255 +#: templates/partials/detail.html:285 #, python-format msgid "page %(p)s" msgstr "%(p)s ページ" -#: templates/partials/detail.html:260 +#: templates/partials/detail.html:290 msgid "Next →" msgstr "次 →" -#: templates/partials/detail.html:266 +#: templates/partials/detail.html:304 +msgid "fit to window" +msgstr "画面に合わせる" + +#: templates/partials/detail.html:306 #, fuzzy, python-format #| msgid "%(n)s row" #| msgid_plural "%(n)s rows" msgid "%(n)s rows" msgstr "%(n)s 行" -#: templates/partials/detail.html:296 +#: templates/partials/detail.html:340 msgid "This table is empty." msgstr "このテーブルは空です。" -#: templates/partials/detail.html:307 +#: templates/partials/detail.html:351 msgid "Truncate table" msgstr "テーブルを truncate" -#: templates/partials/detail.html:308 +#: templates/partials/detail.html:352 msgid "Delete every row. A safety snapshot is taken first." msgstr "全行を削除します。先に安全スナップショットを取得します。" -#: templates/partials/detail.html:311 +#: templates/partials/detail.html:355 #, python-format msgid "" "Truncate "%(schema)s.%(table)s"? This permanently deletes every " @@ -1290,81 +1300,81 @@ msgstr "" ""%(schema)s.%(table)s" を truncate しますか?全行が完全に削除されま" "す。" -#: templates/partials/detail.html:311 templates/partials/detail.html:323 +#: templates/partials/detail.html:355 templates/partials/detail.html:367 #: templates/partials/objects.html:67 templates/partials/objects.html:110 #: templates/partials/objects.html:154 msgid "This runs:" msgstr "実行される SQL:" -#: templates/partials/detail.html:311 templates/partials/detail.html:323 +#: templates/partials/detail.html:355 templates/partials/detail.html:367 msgid "Rows: unknown (this table has never been analyzed)" msgstr "行数: 不明(このテーブルはまだ ANALYZE されていません)" -#: templates/partials/detail.html:311 templates/partials/detail.html:323 +#: templates/partials/detail.html:355 templates/partials/detail.html:367 #, python-format msgid "Rows: about %(n)s (estimated from statistics, last analyzed %(d)s)" msgstr "行数: 約 %(n)s(統計からの推定・最終 ANALYZE %(d)s)" -#: templates/partials/detail.html:311 templates/partials/detail.html:323 +#: templates/partials/detail.html:355 templates/partials/detail.html:367 msgid "Referenced by:" msgstr "参照しているテーブル:" -#: templates/partials/detail.html:311 templates/partials/detail.html:323 +#: templates/partials/detail.html:355 templates/partials/detail.html:367 msgid "A DROP … CASCADE would also drop:" msgstr "DROP … CASCADE で一緒に消えるもの:" -#: templates/partials/detail.html:314 +#: templates/partials/detail.html:358 msgid "Truncate" msgstr "Truncate" -#: templates/partials/detail.html:319 templates/partials/detail.html:326 +#: templates/partials/detail.html:363 templates/partials/detail.html:370 msgid "Drop table" msgstr "テーブルを削除" -#: templates/partials/detail.html:320 +#: templates/partials/detail.html:364 msgid "Remove the table and all its data. A safety snapshot is taken first." msgstr "テーブルと全データを削除します。先に安全スナップショットを取得します。" -#: templates/partials/detail.html:323 +#: templates/partials/detail.html:367 #, python-format msgid "Drop table "%(schema)s.%(table)s"? This cannot be undone." msgstr "" "テーブル "%(schema)s.%(table)s" を削除しますか?元に戻せません。" -#: templates/partials/detail.html:337 +#: templates/partials/detail.html:381 msgid "Rename table" msgstr "テーブルをリネーム" -#: templates/partials/detail.html:337 +#: templates/partials/detail.html:381 msgid "Add column" msgstr "列を追加" -#: templates/partials/detail.html:338 +#: templates/partials/detail.html:382 msgid "Edit column" msgstr "列を編集" -#: templates/partials/detail.html:338 +#: templates/partials/detail.html:382 msgid "Create index" msgstr "インデックスを作成" -#: templates/partials/detail.html:339 templates/partials/detail.html:367 +#: templates/partials/detail.html:383 templates/partials/detail.html:411 msgid "Import CSV" msgstr "CSV インポート" -#: templates/partials/detail.html:350 templates/partials/objects.html:201 +#: templates/partials/detail.html:394 templates/partials/objects.html:201 msgid "New name" msgstr "新しい名前" -#: templates/partials/detail.html:352 templates/partials/detail.html:405 +#: templates/partials/detail.html:396 templates/partials/detail.html:449 #: templates/partials/objects.html:203 msgid "Rename" msgstr "リネーム" -#: templates/partials/detail.html:361 +#: templates/partials/detail.html:405 msgid "CSV file" msgstr "CSV ファイル" -#: templates/partials/detail.html:365 +#: templates/partials/detail.html:409 msgid "" "The first row must be a header; columns are matched to the table by name. " "Rows are appended in one transaction — if any row's types don't fit, the " @@ -1374,43 +1384,43 @@ msgstr "" "は1つのトランザクションで追加され、いずれかの行の型が合わない場合はインポート" "全体がロールバックされます。事前に安全のためのスナップショットを取得します。" -#: templates/partials/detail.html:378 +#: templates/partials/detail.html:422 msgid "column name" msgstr "列名" -#: templates/partials/detail.html:390 +#: templates/partials/detail.html:434 msgid "not null" msgstr "not null" -#: templates/partials/detail.html:402 +#: templates/partials/detail.html:446 msgid "Rename to" msgstr "リネーム先" -#: templates/partials/detail.html:417 +#: templates/partials/detail.html:461 msgid "(current)" msgstr "(現在)" -#: templates/partials/detail.html:420 +#: templates/partials/detail.html:464 msgid "Change" msgstr "変更" -#: templates/partials/detail.html:425 +#: templates/partials/detail.html:469 msgid "Nullability" msgstr "NULL許可" -#: templates/partials/detail.html:433 +#: templates/partials/detail.html:477 msgid "set NOT NULL" msgstr "NOT NULL を設定" -#: templates/partials/detail.html:433 +#: templates/partials/detail.html:477 msgid "drop NOT NULL" msgstr "NOT NULL を解除" -#: templates/partials/detail.html:444 +#: templates/partials/detail.html:488 msgid "Rehearse it" msgstr "リハーサル" -#: templates/partials/detail.html:449 +#: templates/partials/detail.html:493 msgid "" "Runs the real statement and rolls it back. It takes a real ACCESS EXCLUSIVE " "lock while it runs, so readers queue behind it — briefly, but for real." @@ -1418,23 +1428,23 @@ msgstr "" "本物の文を実行して、ロールバックする。実行中は本物の ACCESS EXCLUSIVE ロック" "を取るので、読み取りはその後ろに並ぶ。短いが、本物。" -#: templates/partials/detail.html:458 +#: templates/partials/detail.html:502 msgid "(empty drops it)" msgstr "(空で解除)" -#: templates/partials/detail.html:461 templates/partials/settings_row.html:42 +#: templates/partials/detail.html:505 templates/partials/settings_row.html:42 msgid "Set" msgstr "設定" -#: templates/partials/detail.html:477 +#: templates/partials/detail.html:521 msgid "Click columns in the order you want them indexed." msgstr "インデックスしたい順に列をクリックしてください。" -#: templates/partials/detail.html:491 +#: templates/partials/detail.html:535 msgid "index name" msgstr "インデックス名" -#: templates/partials/detail.html:501 +#: templates/partials/detail.html:545 msgid "built CONCURRENTLY" msgstr "CONCURRENTLY で作成" @@ -2977,16 +2987,16 @@ msgstr "" msgid "Filter tables…" msgstr "テーブルを絞り込み…" -#: templates/partials/table_list.html:17 +#: templates/partials/table_list.html:21 msgid "Statistics have never been collected for this table — run ANALYZE" msgstr "" "このテーブルの統計がまだ集められていません(ANALYZE を実行してください)" -#: templates/partials/table_list.html:17 +#: templates/partials/table_list.html:21 msgid "unknown" msgstr "不明" -#: templates/partials/table_list.html:23 +#: templates/partials/table_list.html:27 #, python-format msgid "%(counter)s table" msgid_plural "%(counter)s tables" diff --git a/templates/base.html b/templates/base.html index 80b305a..ad2d1ed 100644 --- a/templates/base.html +++ b/templates/base.html @@ -42,6 +42,24 @@ .field:focus { outline: none; border-color: #34d399; } .field-sm { padding: .25rem .5rem; font-size: .75rem; } + /* データグリッドの高さを決める数(すべて実測)。**JS の cli2uiFitRows も + CSS の max-height もここを読む**ので、直すときはこの 4 行だけ直す。 + 前は JS に 455、テンプレートに 435 と書き分けていて、片方だけ直して + 1 行はみ出した。 */ + :root { + --grid-chrome: 201px; /* グリッドより上下にあるものの合計 */ + --grid-chrome-open: 313px; /* 絞り込みを開いているとき */ + --grid-head: 33px; /* 列名の行 */ + --grid-row: 37px; /* 1 行の高さ */ + /* 列名の行が 32.5px と半端なので、丸めで 1px あふれてページ側に + スクロールバーが出る。4px 逃がしておく(行数は変わらない)。 */ + --grid-max: calc(100vh - var(--grid-chrome) - 4px); + --grid-max-open: calc(100vh - var(--grid-chrome-open) - 4px); + /* サイドバーの一覧も同じ考えで伸ばす(接続の箱・絞り込み欄・件数の帯ぶんを + 引く)。グリッドと同じ変数から作るので、上の帯を直せば一緒に付いてくる。 */ + --list-max: calc(100vh - var(--grid-chrome) - 30px); + } + /* Top section-nav bar (the top-level section switcher, in the header). The active state is toggled via Alpine :class on top of these base styles. */ .topnav-btn { display: inline-flex; align-items: center; gap: .375rem; white-space: nowrap; @@ -49,6 +67,15 @@ color: #a1a1aa; cursor: pointer; transition: color .15s, background-color .15s; } .topnav-btn:hover { color: #f4f4f5; background: rgba(39,39,42,.6); } .topnav-btn-on { background: #27272a; color: #f4f4f5; } + /* 押すと開くもの(区域メニュー・絞り込み)。枠を付けて「押せるもの」に見せる + ── 文字だけだと見出しに見えて、開くことに気づかれない。開いているあいだは + 枠を光らせる。開閉は aria-expanded で表すので、見た目もそこから作る。 */ + .disclosure { display: inline-flex; align-items: center; gap: .375rem; white-space: nowrap; + border: 1px solid #3f3f46; border-radius: .5rem; background: rgba(39,39,42,.35); + padding: .25rem .5rem; font-size: .75rem; color: #a1a1aa; cursor: pointer; + transition: color .15s, background-color .15s, border-color .15s; } + .disclosure:hover { border-color: #52525b; color: #f4f4f5; } + .disclosure[aria-expanded="true"] { border-color: #71717a; background: #27272a; color: #f4f4f5; } .topnav-sep { width: 1px; height: 1rem; background: #27272a; margin: 0 .375rem; } /* Panel subtabs — the in-panel section switcher (Alpine x-show). */ @@ -93,10 +120,10 @@ // spinner while pg_dump runs, then triggers the browser download from the // blob — and surfaces the server's error text inline if it fails. document.addEventListener("alpine:init", () => { - // Open-state for the overview hover menu (the section bento). A store so - // the bento cards can close it whether they're rendered in the menu or in - // the #detail landing (where there's no local menu scope). - Alpine.store("navmenu", { open: false }); + // どの区域メニューが開いているか('' なら閉。'query' / 'ops' / 'catalog')。 + // 店に置くのは、#detail 側に描かれたベントーのカードからも閉じられるように + // するため(あちらにはメニューのスコープが無い)。 + Alpine.store("navmenu", { open: "" }); Alpine.data("dumpLinks", () => ({ busy: false, @@ -121,7 +148,42 @@ // Filter builder (table detail → Data tab): dynamic column/operator/value // rows. A stable id keys the x-for so editing one row doesn't disturb the // others; null / not-null operators hide the value input. + // データのグリッドを、画面の下端まで伸ばす。 + // + // 固定値(max-h-[60vh] など)だと、上に何があるかで余ったり溢れたりする ── + // 絞り込みを開けば上が伸び、タブを変えれば別のものが載る。**自分の上端を + // 実測して残りを取る**のが唯一ずれない方法。 + // + // これが無いと、グリッドの下を見るのに「まずページを送ってからグリッドを + // 送る」二段階になる。スクロールバーは 1 本にする。 + // 画面に何行入るか。**window.innerHeight だけから決める。** + // + // 描画後のグリッドを測って直す方式にしたら、測る・取り直す・開閉で測り直す、が + // 絡んで挙動が不安定になった(1 回クリックしないと効かない、など)。入る行数は + // 結局ウィンドウの高さで決まるので、そこから引くだけにする。**描画結果に + // 依存しない = 取り直しの連鎖が起きない。** + // + // 寸法は :root のカスタムプロパティから読む。グリッド側の max-height も + // 同じものを使うので、片方だけ直してずれることがない。 + window.cli2uiFitRows = function () { + const px = (name) => + parseFloat(getComputedStyle(document.documentElement).getPropertyValue(name)) || 0; + // 絞り込みを開いていればその高さも引く(開閉自体では取り直さない。 + // 次にページを送ったり件数を変えたときに正しい数になる)。 + const chrome = window.__cli2uiFilterOpen === true ? "--grid-chrome-open" : "--grid-chrome"; + const space = window.innerHeight - px(chrome) - px("--grid-head"); + // 少なく出して余白が残るほうが、1 行ぶんスクロールするより良い。 + return Math.max(5, Math.floor((space - 8) / px("--grid-row"))); + }; + Alpine.data("filterBuilder", () => ({ + // 既定は畳む。絞り込みは毎回使うものではないのに、常に開いていると + // その高さぶんグリッドが下へ押し出される。開いた状態はテーブルを + // またいで覚える(一度使う人は続けて使うため)。 + open: window.__cli2uiFilterOpen === true, + // 開閉で上端は動くが、取り直しはしない。数行の差はグリッドの + // max-height が吸収する(開いているあいだだけ内側に小さくスクロールが出る)。 + toggle() { this.open = !this.open; window.__cli2uiFilterOpen = this.open; }, rows: [{ id: 1, col: "", op: "eq", val: "" }], nextId: 2, add() { this.rows.push({ id: this.nextId++, col: "", op: "eq", val: "" }); }, @@ -144,15 +206,22 @@ })(); - -
-
+
+
cli2ui - + v{{ cli2ui_version }} + {% comment %}区域ナビはロゴと同じ行に置く。前は下に 1 段ぶら下げていて、 + その 40px はグリッドの高さからそのまま引かれていた。{% endcomment %} + {% block topnav %}{% endblock %}
{% csrf_token %} @@ -162,14 +231,9 @@ class="topnav-btn {% if LANGUAGE_CODE == 'ja' %}topnav-btn-on{% endif %}">JP
- {% block topnav %}{% endblock %}
-
+
{% block content %}{% endblock %}
-
- cli2ui - v{{ cli2ui_version }} -
diff --git a/templates/partials/detail.html b/templates/partials/detail.html index 1140034..8bc95fa 100644 --- a/templates/partials/detail.html +++ b/templates/partials/detail.html @@ -13,7 +13,7 @@ ⚠️ 注釈は**タグの外**に置くこと。属性の中に書いた `{#…#}` は Django に解釈されず、 そのまま画面へ出る(2026-09-04 に 2 か所やらかした)。{% endcomment %} -
-
-
-

{{ schema }}.{{ table }}

- {% if table_comment %}

{{ table_comment }}

{% endif %} -
-
- - - - {% trans "backup" %} - .sql ↓ - .dump ↓ - - - - - - {% trans "export" %} - CSV ↓ - JSON ↓ - - - -
-
- {% include "partials/_banner.html" %} - -
- {% if preview %} - - {% comment %}ページャはグリッドの上に貼り付ける。グリッドだけがスクロールする - ので流れはしないが、1000 行を送った先で「いま何ページ目か」を見に戻るのに - 上まで戻る必要はない。{% endcomment %} -
- - {% blocktrans with a=preview.first_row|intcomma b=preview.last_row|intcomma %}rows {{ a }}–{{ b }}{% endblocktrans %} - {% if preview.estimated_rows is not None %} - / {% blocktrans with n=preview.estimated_rows|intcomma %}of about {{ n }} (estimated from statistics){% endblocktrans %} - {% else %} - / {% trans "of an unknown total — this table has never been analyzed" %} - {% endif %} - - - {% if not preview.stable %} - - {% trans "⚠ No primary key — row order is not guaranteed between pages" %} - - {% endif %} - - - {% if preview.has_previous %} - - {% endif %} - - {% if preview.estimated_pages %} - {% blocktrans with p=preview.page|intcomma t=preview.estimated_pages|intcomma %}page {{ p }} of about {{ t }}{% endblocktrans %} - {% else %} - {% blocktrans with p=preview.page|intcomma %}page {{ p }}{% endblocktrans %} - {% endif %} - - {% if preview.has_next %} - - {% endif %} - - -
- {% endif %}
@@ -284,11 +216,16 @@

{{ schema }}.{{ table }}

{% comment %}グリッドだけが縦にスクロールする。1000 行あるので、ページ全体が 動くと列名もページ送りも画面の外へ流れてしまう。SQL ランナーと絞り込みの 結果は既にこうなっていて、ここだけ揃っていなかった。{% endcomment %} -
+ {% comment %}高さは行数と同じ定数から作る(行数 = (画面高 - 470) / 33、 + 高さ = 画面高 - 440 = 引いた残り + 見出し 30)。絞り込みを開くと + その高さぶん上端が下がるので、開いているあいだだけ狭める ── 取り直さず + CSS で吸収する。{% endcomment %} +
- {% for c in preview_columns %}{% endfor %} + {% for name, type in preview_header %}{% endfor %} @@ -340,6 +277,114 @@

{{ schema }}.{{ table }}

+ {% comment %}底の帯は 1 本。左が「いま何行目か」とページ送り、右がテーブル単位の + 操作。前は行数をグリッドの上、操作をさらにその上、と 2 段積んでいた。上に積んだ + ものはそのぶん行数が減るので、下へ回してまとめた(Sequel Ace も底の 1 本に + 「テーブルから 1 - 1,000 行目」と操作を並べている)。{% endcomment %} +
+ {% if preview %} + +
+ {% comment %}帯は 1 行に収める。折り返すとその高さぶんグリッドが縮むので、 + 説明は title に逃がして画面には数字だけ出す。{% endcomment %} + {% if preview.estimated_rows is not None %} + + {% blocktrans with a=preview.first_row|intcomma b=preview.last_row|intcomma %}rows {{ a }}–{{ b }}{% endblocktrans %} + / {% blocktrans with n=preview.estimated_rows|intcomma %}about {{ n }}{% endblocktrans %} + + {% else %} + + {% blocktrans with a=preview.first_row|intcomma b=preview.last_row|intcomma %}rows {{ a }}–{{ b }}{% endblocktrans %} + / {% trans "total unknown" %} + + {% endif %} + + {% if not preview.stable %} + + {% trans "⚠ no primary key" %} + + {% endif %} + + + {% if preview.has_previous %} + + {% endif %} + + {% if preview.estimated_pages %} + {% blocktrans with p=preview.page|intcomma t=preview.estimated_pages|intcomma %}p. {{ p }} / ~{{ t }}{% endblocktrans %} + {% else %} + {% blocktrans with p=preview.page|intcomma %}p. {{ p }}{% endblocktrans %} + {% endif %} + + {% if preview.has_next %} + + {% endif %} + {% comment %}既定は「画面に合わせる」。何行入るかはブラウザでしか + 分からないので、選ぶたびに測って `fit=` で渡す。サーバは受け取った数を + 範囲に丸めてから使う。{% endcomment %} + + +
+ {% endif %} + {% comment %}帯は 1 行に収めたい。よく押す「SQL で開く」だけ出して、残りは ⋯ に + しまう(バックアップ・エクスポート・インポート・リネームは、開いてすぐ押すもの + ではない)。上へ開くのは帯が窓の底にあるため。{% endcomment %} +
+ +
+ +
+ + + {% trans "backup" %} + .sql ↓ + .dump ↓ + + + + + + {% trans "export" %} + CSV ↓ + JSON ↓ + + + +
+
+
+
+
{{ c }}{{ name }}{{ type }}
diff --git a/templates/partials/query_result.html b/templates/partials/query_result.html index 4ac045a..53a78d1 100644 --- a/templates/partials/query_result.html +++ b/templates/partials/query_result.html @@ -39,7 +39,7 @@ {% if result.columns %}
-
+
diff --git a/templates/partials/table_list.html b/templates/partials/table_list.html index 9dabfc7..c143385 100644 --- a/templates/partials/table_list.html +++ b/templates/partials/table_list.html @@ -5,10 +5,13 @@ class="w-full bg-zinc-950 border border-zinc-700 rounded-lg px-3 py-1.5 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-emerald-400"> -
    +
      {% for t in tables %}
    • + {% comment %}前に測った「画面に入る行数」を一緒に送る。2 つ目以降のテーブルは + 1 回で正しい行数になる(1 つ目だけは測る前なので、開いた直後に直す)。{% endcomment %} +{% comment %}区域は名前を出したまま並べる。前は「⌂ 概要 ▾」1 つにホバーで開く +メガメニューを隠していて、そこに何かあること自体に気づけなかった。3 つのグループ名を +常に出しておけば、押す前に「クエリ・稼働・カタログの 3 面がある」ことが読める。 +開くのはクリックにした(ホバーだと通りすがりに開いて邪魔になる)。{% endcomment %} +