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
16 changes: 10 additions & 6 deletions tools/developer_tools/bely-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,10 +257,14 @@ All three levels render as full-width, aligned tables (rows stay in API order, n

| Level | Columns |
|-------|---------|
| Logbook | Name, Display, Description |
| Document | Name, Description, Systems, Owner, Modified |
| Logbook | Display, Description |
| Document | Name, Systems, Owner, Modified |
| Entry | Date, Author, Entry (a snippet of the first line) — replies render as indented rows beneath their parent, expanded by default |

Logbook types render as an indented hierarchy in API order. Grouping parents are
informational; select a leaf to browse or create documents. Filtering includes hierarchy
labels and type metadata.

Press `i` at the logbook/document levels to open a side info panel with a few extra fields
for the highlighted row (it splits the table's width; `i` again closes it). Entries always
show a preview pane alongside the table — the entry body rendered as markdown (headings,
Expand Down Expand Up @@ -297,15 +301,15 @@ on — `i` disappears once you drill into entries, and `s` / `y` / `e` / `f` / `
| `s` | Entries level only: save the highlighted entry's markdown to a file in the current directory. |
| `y` | Entries level only: copy a `bely-cli entry get` reference for the highlighted entry to the clipboard. |
| `e` | Entries level only: open the highlighted entry in `$EDITOR`; if you change it, offers to save the result back to the server (a mutation, so this is where the app authenticates if it hasn't already). |
| `p` | Entries level only: reply to the highlighted entry's top-level thread. Attachments are supported. |
| `t` | Entries level only: collapse/expand the reply thread under the highlighted entry (or its parent, if the highlight is on a reply). Replies start expanded. |
| `i` | Logbook/document levels only: toggle the side info panel. |
| `f` | Entries level only: toggle the table to widen the preview pane. |
| `r` | Refresh the current level, bypassing the in-session cache. Collapsed threads stay collapsed. |
| `r` | Refresh the current level, bypassing the in-session cache. Entry selection, preview scroll, filters, and collapsed threads are preserved when possible. |
| `q` | Quit without selecting. |

Replies only ever nest one level deep — the server doesn't return replies-to-replies — and
`n` on a highlighted reply adds a new top-level entry, not a reply to that reply (there's no
API for that yet).
Replies only nest one level deep. Pressing `p` on either a top-level entry or one of its
replies targets the top-level thread; `n` still adds a separate top-level entry.

On selecting an entry the TUI exits and prints its `doc-id` / `log-id`, plus a ready-to-run
`bely-cli entry get` command so you can fetch it:
Expand Down
9 changes: 7 additions & 2 deletions tools/developer_tools/bely-cli/src/bely_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ def get_token_file():


def get_host():
"""Return the BELY server URL from env var or settings."""
"""Return the BELY server URL from env var or settings without trailing slashes."""
host = os.environ.get("BELY_HOST") or get_setting("host")
if not host:
raise ValueError("no host configured. Set BELY_HOST or add 'host' to settings.yaml.")
host = host.rstrip("/")
if not host:
raise ValueError("BELY host must not contain only slashes.")
return host


Expand Down Expand Up @@ -130,7 +133,9 @@ def login(username, password):
except belyApi.exceptions.UnauthorizedException:
raise ValueError(f"Authentication failed: invalid credentials for user '{username}'")
except Exception as e:
raise RuntimeError(f"Authentication failed: {e}") from e
from .common import format_error_message

raise RuntimeError(f"Authentication failed: {format_error_message(e, factory)}") from e
save_token(factory.get_authenticate_token())
return factory

Expand Down
4 changes: 2 additions & 2 deletions tools/developer_tools/bely-cli/src/bely_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import click

from .common import FORMATS, set_no_prompt
from .common import FORMATS, format_error_message, set_no_prompt
from .config import VALID_FIELDS
from .commands import (
cmd_new_doc,
Expand Down Expand Up @@ -212,7 +212,7 @@ def main():
try:
cli()
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
print(f"Error: {format_error_message(e)}", file=sys.stderr)
sys.exit(1)


Expand Down
21 changes: 21 additions & 0 deletions tools/developer_tools/bely-cli/src/bely_cli/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,27 @@ def is_no_prompt():
return _no_prompt


def format_error_message(error, factory=None):
"""Return a concise server message for API errors, or the normal exception text."""
try:
import belyApi

if not isinstance(error, belyApi.exceptions.ApiException):
return str(error)
except Exception:
return str(error)

try:
if factory is None:
from . import auth

factory = auth.get_factory()
parsed = factory.parse_api_exception(error)
return parsed.message or str(error)
except Exception:
return str(error)


def print_items(items, columns, fmt="text"):
"""Print a list of dicts as a table, JSON array, or YAML sequence.

Expand Down
2 changes: 1 addition & 1 deletion tools/developer_tools/bely-cli/src/bely_cli/tui/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def __init__(self, logbook_api, download_api=None):

def logbook_types(self):
if self._types is None:
self._types = self._logbook_api.get_logbook_types()
self._types = self._logbook_api.get_logbook_type_hierarchy()
return self._types

def logbook_systems(self):
Expand Down
73 changes: 58 additions & 15 deletions tools/developer_tools/bely-cli/src/bely_cli/tui/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,46 @@
# -- list-row formatting (used by both the old curses UI and the new
# Textual OptionList rows) --

class TypeNode(NamedTuple):
"""One flattened hierarchy row wrapping its original EntityType."""

entity: object
depth: int
branch: str
selectable: bool
hierarchy_text: str


def flatten_types(types):
"""Flatten EntityType children depth-first while retaining hierarchy guides."""
rows = []

def walk(items, depth, prefix_lasts, ancestors):
for index, entity in enumerate(items or []):
is_last = index == len(items) - 1
branch = _branch_prefix(prefix_lasts, is_last) if depth else ""
children = getattr(entity, "entity_type_children", None) or []
name = getattr(entity, "name", None) or ""
hierarchy_text = " / ".join(ancestors + [name])
rows.append(TypeNode(entity, depth, branch, not children, hierarchy_text))
if children:
next_prefix = prefix_lasts + [is_last] if depth else []
walk(children, depth + 1, next_prefix, ancestors + [name])

walk(types, 0, [], [])
return rows


def type_entity(t):
return t.entity if isinstance(t, TypeNode) else t


def format_type(t):
"""Display string for a logbook type (EntityType)."""
display = getattr(t, "display_name", None) or ""
name = getattr(t, "name", None) or ""
return f"{name} ({display})" if display else name
"""Display name for a logbook type, with hierarchy guides when present."""
node = t if isinstance(t, TypeNode) else None
entity = type_entity(t)
label = getattr(entity, "display_name", None) or getattr(entity, "name", None) or ""
return f"{node.branch}{label}" if node else label


def format_doc(d):
Expand Down Expand Up @@ -65,27 +100,29 @@ def _doc_owner(d):
return getattr(more_info, "owner_username", None) or ""


TYPE_COLUMNS = [("Name", 24), ("Display", 24), ("Description", None)]
TYPE_COLUMNS = [("Display", 32), ("Description", None)]


def type_row(t):
"""DataTable row cells for a logbook type (EntityType)."""
name = getattr(t, "name", None) or ""
display = getattr(t, "display_name", None) or ""
description = getattr(t, "description", None) or ""
return (name, display, description)
"""DataTable row cells for a logbook type (EntityType or TypeNode)."""
node = t if isinstance(t, TypeNode) else None
entity = type_entity(t)
display = getattr(entity, "display_name", None) or getattr(entity, "name", None) or ""
if node:
display = node.branch + display
description = getattr(entity, "description", None) or ""
return (display, description)


DOC_COLUMNS = [
("Name", 32), ("Description", None), ("Systems", 20), ("Owner", 14), ("Modified", 16),
("Name", None), ("Systems", 20), ("Owner", 14), ("Modified", 16),
]


def doc_row(d):
"""DataTable row cells for a log document (ItemDomainLogbook)."""
name = getattr(d, "name", None) or "(unnamed)"
description = getattr(d, "description", None) or ""
return (name, description, _doc_systems(d), _doc_owner(d), _doc_modified(d))
return (name, _doc_systems(d), _doc_owner(d), _doc_modified(d))


ENTRY_COLUMNS = [("Date", 16), ("Author", 16), ("Entry", None)]
Expand Down Expand Up @@ -220,8 +257,13 @@ def summarize_reactions(reactions):
def entry_metadata_rows(entry, doc, parent=None):
"""[(label, value)] metadata rows for the entry preview header."""
rows = [("log_id", str(getattr(entry, "log_id", "") or ""))]
if parent is not None:
rows.append(("reply to", str(getattr(parent, "log_id", "") or "")))
doc_id = getattr(entry, "item_id", None) or getattr(doc, "id", None) or ""
rows.append(("log_doc_id", str(doc_id)))
parent_id = getattr(entry, "parent_log_id", None)
if parent_id is None and parent is not None:
parent_id = getattr(parent, "log_id", None)
if parent_id is not None:
rows.append(("parent_log_id", str(parent_id)))
rows.append(("doc", getattr(doc, "name", None) or ""))

entered_by = getattr(entry, "entered_by_username", None) or ""
Expand Down Expand Up @@ -284,6 +326,7 @@ def doc_metadata_rows(doc):

def type_metadata_rows(t):
"""[(label, value)] metadata rows for the logbook-type preview header."""
t = type_entity(t)
rows = [("name", getattr(t, "name", None) or "")]

display_name = getattr(t, "display_name", None)
Expand Down
Loading
Loading