feat: expand shusub2 dashboard columns
This commit is contained in:
@@ -84,10 +84,12 @@ Environment variables override the config file:
|
|||||||
`shusub2` opens a single dashboard with Accounts, Keys, Logs, and Errors
|
`shusub2` opens a single dashboard with Accounts, Keys, Logs, and Errors
|
||||||
tables stacked vertically. Press `a`, `k`, `l`, or `e` to focus a table, `/` to
|
tables stacked vertically. Press `a`, `k`, `l`, or `e` to focus a table, `/` to
|
||||||
filter all four tables, and `r` to refresh all data. The two-line detail area
|
filter all four tables, and `r` to refresh all data. The two-line detail area
|
||||||
follows the selected row.
|
follows the selected row. Accounts, Keys, Logs, and Errors use green, yellow,
|
||||||
|
magenta, and red section styling respectively.
|
||||||
|
|
||||||
The dashboard uses compact columns sized to fit an 80-column terminal without
|
The dashboard sizes every column from the fetched content so wide terminals show
|
||||||
horizontal scrolling:
|
complete keys, accounts, and models. On a narrow terminal, a table scrolls
|
||||||
|
horizontally instead of truncating a field:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
ACCOUNT | Group | Today | Daily | 5h | 7d | Avail
|
ACCOUNT | Group | Today | Daily | 5h | 7d | Avail
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "shusub2"
|
name = "shusub2"
|
||||||
version = "0.2.8"
|
version = "0.2.9"
|
||||||
description = "Terminal UI for Sub2API account quota and daily usage"
|
description = "Terminal UI for Sub2API account quota and daily usage"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
+10
-37
@@ -19,7 +19,7 @@ from typing import Any
|
|||||||
|
|
||||||
|
|
||||||
APP_NAME = "shusub2"
|
APP_NAME = "shusub2"
|
||||||
FALLBACK_VERSION = "0.2.8"
|
FALLBACK_VERSION = "0.2.9"
|
||||||
DEFAULT_API_URL = "http://127.0.0.1:18318/api/tui/accounts"
|
DEFAULT_API_URL = "http://127.0.0.1:18318/api/tui/accounts"
|
||||||
DEFAULT_CONFIG_FILE = "~/.config/shusub2/api-url"
|
DEFAULT_CONFIG_FILE = "~/.config/shusub2/api-url"
|
||||||
DEFAULT_STATUS_CONFIG_FILE = "~/.config/shusub2/status-url"
|
DEFAULT_STATUS_CONFIG_FILE = "~/.config/shusub2/status-url"
|
||||||
@@ -1258,57 +1258,30 @@ def run_textual(
|
|||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def configure_table(table: DataTable, columns: tuple[tuple[str, int], ...]) -> None:
|
def configure_table(table: DataTable, columns: tuple[str, ...]) -> None:
|
||||||
table.cursor_type = "row"
|
table.cursor_type = "row"
|
||||||
table.zebra_stripes = True
|
table.zebra_stripes = True
|
||||||
for label, width in columns:
|
# Content-sized columns preserve complete values on wide terminals;
|
||||||
table.add_column(label, width=width)
|
# narrow terminals use DataTable's horizontal scrolling instead.
|
||||||
|
for label in columns:
|
||||||
|
table.add_column(label)
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
self.configure_table(
|
self.configure_table(
|
||||||
self.query_one("#accounts", DataTable),
|
self.query_one("#accounts", DataTable),
|
||||||
(
|
("ACCOUNT", "Group", "Today", "Daily", "5h", "7d", "Avail"),
|
||||||
("ACCOUNT", 16),
|
|
||||||
("Group", 5),
|
|
||||||
("Today", 8),
|
|
||||||
("Daily", 10),
|
|
||||||
("5h", 9),
|
|
||||||
("7d", 9),
|
|
||||||
("Avail", 7),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
self.configure_table(
|
self.configure_table(
|
||||||
self.query_one("#keys", DataTable),
|
self.query_one("#keys", DataTable),
|
||||||
(
|
("KEY", "Today", "Tokens", "Req"),
|
||||||
("KEY", 16),
|
|
||||||
("Today", 9),
|
|
||||||
("Tokens", 8),
|
|
||||||
("Req", 6),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
self.configure_table(
|
self.configure_table(
|
||||||
self.query_one("#logs", DataTable),
|
self.query_one("#logs", DataTable),
|
||||||
(
|
("LOG KEY", "Account", "Model", "Cost", "Latency", "Time", "Age"),
|
||||||
("LOG KEY", 10),
|
|
||||||
("Account", 10),
|
|
||||||
("Model", 12),
|
|
||||||
("Cost", 7),
|
|
||||||
("Latency", 7),
|
|
||||||
("Time", 11),
|
|
||||||
("Age", 7),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
self.configure_table(
|
self.configure_table(
|
||||||
self.query_one("#errors", DataTable),
|
self.query_one("#errors", DataTable),
|
||||||
(
|
("ERR", "Status", "Key", "Account", "Model", "Time", "Age"),
|
||||||
("ERR", 4),
|
|
||||||
("Status", 6),
|
|
||||||
("Key", 12),
|
|
||||||
("Account", 11),
|
|
||||||
("Model", 12),
|
|
||||||
("Time", 11),
|
|
||||||
("Age", 8),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
self.refresh_all(refresh=True)
|
self.refresh_all(refresh=True)
|
||||||
self.set_interval(refresh_seconds, self.refresh_accounts)
|
self.set_interval(refresh_seconds, self.refresh_accounts)
|
||||||
|
|||||||
+14
-8
@@ -678,7 +678,7 @@ class Sub2APILogsTests(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
|
class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
|
||||||
async def test_dashboard_fits_three_tables_at_80_by_24_and_focuses_each(self) -> None:
|
async def test_dashboard_supports_wide_columns_at_80_by_24_and_focuses_each(self) -> None:
|
||||||
from textual.app import App
|
from textual.app import App
|
||||||
|
|
||||||
mod = load_module()
|
mod = load_module()
|
||||||
@@ -688,7 +688,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
"accounts": [
|
"accounts": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "account-one",
|
"name": "account-one-with-a-complete-expanded-dashboard-name",
|
||||||
"routing_group": "fast",
|
"routing_group": "fast",
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
"kind": "pay_as_you_go",
|
"kind": "pay_as_you_go",
|
||||||
@@ -708,9 +708,9 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"api_key_name": "key-one",
|
"api_key_name": "wmy-production-key-with-a-complete-expanded-name",
|
||||||
"account_name": "account-one",
|
"account_name": "account-one-with-a-complete-expanded-dashboard-name",
|
||||||
"model": "gpt-5.5-codex",
|
"model": "gpt-5.5-codex-with-a-complete-expanded-dashboard-model-name",
|
||||||
"duration_ms": 1200,
|
"duration_ms": 1200,
|
||||||
"created_at": "2026-07-24T12:00:00+08:00",
|
"created_at": "2026-07-24T12:00:00+08:00",
|
||||||
}
|
}
|
||||||
@@ -723,8 +723,9 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
"_node": "cn",
|
"_node": "cn",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"status_code": 500,
|
"status_code": 500,
|
||||||
"account_name": "account-one",
|
"api_key_name": "wmy-production-key-with-a-complete-expanded-name",
|
||||||
"requested_model": "gpt-5.5-codex",
|
"account_name": "account-one-with-a-complete-expanded-dashboard-name",
|
||||||
|
"requested_model": "gpt-5.5-codex-with-a-complete-expanded-dashboard-model-name",
|
||||||
"phase": "upstream",
|
"phase": "upstream",
|
||||||
"type": "api_error",
|
"type": "api_error",
|
||||||
"created_at": "2026-07-24T12:01:00+08:00",
|
"created_at": "2026-07-24T12:01:00+08:00",
|
||||||
@@ -767,7 +768,12 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
tables = [screen.query_one(selector) for selector in ("#accounts", "#keys", "#logs", "#errors")]
|
tables = [screen.query_one(selector) for selector in ("#accounts", "#keys", "#logs", "#errors")]
|
||||||
for table in tables:
|
for table in tables:
|
||||||
self.assertGreaterEqual(table.region.height, 4)
|
self.assertGreaterEqual(table.region.height, 4)
|
||||||
self.assertLessEqual(table.virtual_size.width, table.region.width)
|
logs_table = screen.query_one("#logs")
|
||||||
|
errors_table = screen.query_one("#errors")
|
||||||
|
self.assertGreater(logs_table.virtual_size.width, logs_table.region.width)
|
||||||
|
self.assertGreater(errors_table.virtual_size.width, errors_table.region.width)
|
||||||
|
self.assertGreater(logs_table.max_scroll_x, 0)
|
||||||
|
self.assertGreater(errors_table.max_scroll_x, 0)
|
||||||
self.assertEqual(len({table.styles.background for table in tables}), 4)
|
self.assertEqual(len({table.styles.background for table in tables}), 4)
|
||||||
self.assertIn("wmy", str(screen.query_one("#summary").render()))
|
self.assertIn("wmy", str(screen.query_one("#summary").render()))
|
||||||
for key, expected_id in (("k", "keys"), ("l", "logs"), ("e", "errors"), ("a", "accounts")):
|
for key, expected_id in (("k", "keys"), ("l", "logs"), ("e", "errors"), ("a", "accounts")):
|
||||||
|
|||||||
Reference in New Issue
Block a user