feat(UX-001): add full-width two-pane dashboard layout
This commit is contained in:
@@ -5,7 +5,7 @@ from nicegui import ui
|
||||
|
||||
from app.domain.backtesting_math import asset_quantity_from_workspace_config
|
||||
from app.models.workspace import get_workspace_repository
|
||||
from app.pages.common import dashboard_page, render_workspace_recovery
|
||||
from app.pages.common import dashboard_page, split_page_panes
|
||||
from app.services.event_comparison_ui import EventComparisonPageService
|
||||
|
||||
|
||||
@@ -72,9 +72,14 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
||||
"event-comparison",
|
||||
workspace_id=workspace_id,
|
||||
):
|
||||
with ui.row().classes("w-full gap-6 max-lg:flex-col"):
|
||||
left_pane, right_pane = split_page_panes(
|
||||
left_testid="event-comparison-left-pane",
|
||||
right_testid="event-comparison-right-pane",
|
||||
)
|
||||
|
||||
with left_pane:
|
||||
with ui.card().classes(
|
||||
"w-full max-w-xl rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
||||
"w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
||||
):
|
||||
ui.label("Comparison Form").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
|
||||
ui.label(
|
||||
@@ -112,6 +117,11 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
||||
validation_label = ui.label("").classes("text-sm text-rose-600 dark:text-rose-300")
|
||||
run_button = ui.button("Run comparison").props("color=primary")
|
||||
|
||||
selected_summary = ui.card().classes(
|
||||
"w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
||||
)
|
||||
|
||||
with right_pane:
|
||||
result_panel = ui.column().classes("w-full gap-6")
|
||||
|
||||
def selected_template_slugs() -> tuple[str, ...]:
|
||||
@@ -120,11 +130,35 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
||||
return (raw_value,) if raw_value else ()
|
||||
return tuple(str(item) for item in raw_value if item)
|
||||
|
||||
def render_selected_summary(entry_spot: float | None = None) -> None:
|
||||
selected_summary.clear()
|
||||
with selected_summary:
|
||||
ui.label("Scenario Summary").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
|
||||
with ui.grid(columns=1).classes("w-full gap-4 sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2"):
|
||||
cards = [
|
||||
(
|
||||
"Preset",
|
||||
preset_select_options.get(str(preset_select.value), str(preset_select.value or "—")),
|
||||
),
|
||||
("Templates", str(len(selected_template_slugs()))),
|
||||
("Underlying units", f"{float(units_input.value or 0.0):,.0f}"),
|
||||
("Loan amount", f"${float(loan_input.value or 0.0):,.0f}"),
|
||||
("Margin call LTV", f"{float(ltv_input.value or 0.0):.1%}"),
|
||||
("Entry spot", f"${entry_spot:,.2f}" if entry_spot is not None else "Pending"),
|
||||
]
|
||||
for label, value in cards:
|
||||
with ui.card().classes(
|
||||
"rounded-xl border border-slate-200 bg-slate-50 p-4 shadow-none dark:border-slate-800 dark:bg-slate-950"
|
||||
):
|
||||
ui.label(label).classes("text-sm text-slate-500 dark:text-slate-400")
|
||||
ui.label(value).classes("text-xl font-bold text-slate-900 dark:text-slate-100")
|
||||
|
||||
def refresh_preset_details() -> None:
|
||||
option = preset_lookup.get(str(preset_select.value or ""))
|
||||
if option is None:
|
||||
metadata_label.set_text("")
|
||||
scenario_label.set_text("")
|
||||
render_selected_summary(entry_spot=None)
|
||||
return
|
||||
template_select.value = list(service.default_template_selection(str(option["slug"])))
|
||||
try:
|
||||
@@ -153,6 +187,7 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
||||
except (ValueError, KeyError) as exc:
|
||||
metadata_label.set_text(f"Preset: {option['label']} — {option['description']}")
|
||||
scenario_label.set_text(str(exc))
|
||||
render_selected_summary(entry_spot=None)
|
||||
return
|
||||
preset = service.event_preset_service.get_preset(str(option["slug"]))
|
||||
metadata_label.set_text(f"Preset: {option['label']} — {option['description']}")
|
||||
@@ -166,6 +201,16 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
||||
)
|
||||
+ f" · Entry spot: ${scenario.initial_portfolio.entry_spot:,.2f}"
|
||||
)
|
||||
render_selected_summary(entry_spot=float(scenario.initial_portfolio.entry_spot))
|
||||
|
||||
def render_result_validation(message: str) -> None:
|
||||
result_panel.clear()
|
||||
with result_panel:
|
||||
with ui.card().classes(
|
||||
"w-full rounded-2xl border border-amber-200 bg-amber-50 shadow-sm dark:border-amber-900/60 dark:bg-amber-950/30"
|
||||
):
|
||||
ui.label("Scenario Results").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
|
||||
ui.label(message).classes("text-sm text-amber-800 dark:text-amber-200")
|
||||
|
||||
def render_report() -> None:
|
||||
validation_label.set_text("")
|
||||
@@ -180,9 +225,12 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
||||
)
|
||||
except (ValueError, KeyError) as exc:
|
||||
validation_label.set_text(str(exc))
|
||||
render_result_validation(str(exc))
|
||||
return
|
||||
except Exception:
|
||||
validation_label.set_text("Event comparison failed. Please verify the seeded inputs and try again.")
|
||||
message = "Event comparison failed. Please verify the seeded inputs and try again."
|
||||
validation_label.set_text(message)
|
||||
render_result_validation(message)
|
||||
return
|
||||
|
||||
preset = report.event_preset
|
||||
@@ -195,13 +243,14 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
||||
f"{scenario.start_date.isoformat()} → {scenario.end_date.isoformat()} · "
|
||||
f"Entry spot: ${scenario.initial_portfolio.entry_spot:,.2f}"
|
||||
)
|
||||
render_selected_summary(entry_spot=float(scenario.initial_portfolio.entry_spot))
|
||||
chart_model = service.chart_model(report)
|
||||
|
||||
with result_panel:
|
||||
with ui.card().classes(
|
||||
"w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
||||
):
|
||||
ui.label("Scenario Metadata").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
|
||||
ui.label("Scenario Results").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
|
||||
with ui.grid(columns=4).classes("w-full gap-4 max-lg:grid-cols-2 max-sm:grid-cols-1"):
|
||||
cards = [
|
||||
("Symbol", scenario.symbol),
|
||||
@@ -292,6 +341,10 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
||||
).classes("h-96 w-full")
|
||||
|
||||
preset_select.on_value_change(lambda _: refresh_preset_details())
|
||||
template_select.on_value_change(lambda _: refresh_preset_details())
|
||||
units_input.on_value_change(lambda _: refresh_preset_details())
|
||||
loan_input.on_value_change(lambda _: refresh_preset_details())
|
||||
ltv_input.on_value_change(lambda _: refresh_preset_details())
|
||||
run_button.on_click(lambda: render_report())
|
||||
refresh_preset_details()
|
||||
render_report()
|
||||
|
||||
Reference in New Issue
Block a user