[Python] How to correctly capture the updated layout from a streamlit-elements dashboard.Grid...

Discussão em 'Python' iniciado por Stack, Agosto 7, 2025.

  1. Stack

    Stack Membro Participativo

    I am developing a financial dashboard using Streamlit and the streamlit-elements library. My goal is to create a user-customizable dashboard where users can drag and resize cards. The final layout (the positions and sizes of all cards) should be saved to a Supabase (PostgreSQL) database so that it can be restored the next time the user logs in.

    The Problem:

    I am able to save user preferences for data filters (another jsonb column in the same database table) without any issues. However, I am unable to correctly capture the modified layout.

    When a user moves or resizes a card in the UI and clicks the "Save Preferences" button, the layout that gets saved to Supabase is always the initial, default layout, not the new layout with the user's modifications.

    What I've Already Tried and Verified:

    onChange Callback: My initial approach was to use the onChange parameter of the dashboard.Grid to trigger a Python function that updates a st.session_state.working_layout variable. Debugging showed that, due to a race condition, the button's script rerun is processed before the onChange callback updates the session state, so the old layout is always captured.

    sync() Function: I tried using the sync() function from streamlit-elements at the beginning of the button's logic to force an update from the frontend. This also failed to capture the updated layout in the session state variable.

    Supabase Permissions (RLS): I have thoroughly verified that my Supabase connection, API keys, and Row Level Security (RLS) policies are correct. Crucially, I created a separate test script that successfully writes a hardcoded JSON object to the exact same saved_layouts column. This proves that the issue is not with the database connection or permissions, but strictly with capturing the state from the dashboard.Grid component within the main app.

    Direct State Binding: I also tried removing the onChange callback and binding the dashboard.Grid directly to a session state variable (st.session_state.layout). This also failed to capture the changes when the save button was pressed.

    My conclusion is that I am fundamentally misunderstanding how to reliably get the latest state of the grid from the frontend (React/JS) into the backend (Python) before processing the save logic.

    My Question:

    Given the code below, what is the correct and robust way to capture the latest layout (with the user's modifications) from the dashboard.Grid and have it available in a session state variable at the moment the "Save Preferences" button is clicked?

    Code (Dash.py): To reproduce this, you can replace the render_*_card functions with a simple function that returns a placeholder string, like def render_cds_table_card(supabase, filters1, filters2): return "Card Content". The core issue lies within the Dash.py structure itself.

    Python

    # Dash.py
    import streamlit as st
    import streamlit.components.v1 as components
    import datetime
    import time
    from supabase import create_client, Client
    from gotrue.errors import AuthApiError
    from streamlit_autorefresh import st_autorefresh
    from streamlit_elements import elements, dashboard, mui, html
    import copy

    # Assume these modules exist and provide the necessary variables and functions
    from cds_card import render_cds_table_card, CDS_TERMS
    from commodities_card import render_commodities_table_card, COMMODITY_NAMES
    # ... import other card modules ...

    # --- Global Constants ---
    CDS_COUNTRIES = ['BR', 'US', 'DE', 'CH', 'JP', 'CN']
    # ... other constants ...

    # --- Page Config ---
    st.set_page_config(page_title="Dashboard Financeiro", layout="wide", initial_sidebar_state="collapsed")

    # --- CSS ---
    st.markdown("""<style>
    /* ... (CSS styles from your file) ... */
    </style>""", unsafe_allow_html=True)


    # --- Application Functions ---

    @st.cache_resource
    def init_supabase_client():
    """Initializes and caches the Supabase client."""
    try:
    return create_client(st.secrets["supabase_url"], st.secrets["supabase_key"])
    except Exception as e:
    st.error(f"Failed to connect to Supabase: {e}")
    return None

    def initialize_session():
    """Initializes session state variables if they don't exist."""
    if 'auth_user' not in st.session_state: st.session_state.auth_user = None
    if 'active_desktop' not in st.session_state: st.session_state.active_desktop = "Desktop 1"
    if 'user_filters' not in st.session_state: st.session_state.user_filters = {}
    if 'filters_loaded' not in st.session_state: st.session_state.filters_loaded = False
    if 'saved_layouts' not in st.session_state: st.session_state.saved_layouts = {}
    if 'working_layout' not in st.session_state: st.session_state.working_layout = {}

    def handle_layout_change(new_layout):
    """Callback that is supposed to update the working layout."""
    desktop_key = st.session_state.active_desktop
    st.session_state.working_layout[desktop_key] = new_layout

    def show_login_screen(supabase: Client):
    """Renders the login screen."""
    # ... (login logic) ...

    def show_dashboard(supabase: Client):
    """Renders the main dashboard."""
    if not st.session_state.auth_user or not hasattr(st.session_state.auth_user, 'user'):
    st.session_state.clear(); st.rerun(); return

    user_info = st.session_state.auth_user.user

    DEFAULT_LAYOUT = [
    {'i': 'cds_card', 'x': 0, 'y': 0, 'w': 24, 'h': 32},
    # ... (all other default card positions) ...
    {'i': 'bolsas_card', 'x': 72, 'y': 32, 'w': 24, 'h': 32},
    ]

    if not st.session_state.filters_loaded:
    try:
    profile_response = supabase.table("profiles").select("saved_filters, saved_layouts").eq("id", user_info.id).maybe_single().execute()

    st.session_state.user_filters = profile_response.data.get('saved_filters', {}) if profile_response.data else {}

    saved_layouts_from_db = profile_response.data.get('saved_layouts') if profile_response.data else None
    if saved_layouts_from_db:
    st.session_state.saved_layouts = saved_layouts_from_db
    else:
    st.session_state.saved_layouts = {"Desktop 1": DEFAULT_LAYOUT}

    st.session_state.working_layout = copy.deepcopy(st.session_state.saved_layouts)
    st.session_state.filters_loaded = True
    except Exception as e:
    st.warning(f"Could not load preferences: {e}")
    st.session_state.user_filters = {}
    st.session_state.saved_layouts = {"Desktop 1": DEFAULT_LAYOUT}
    st.session_state.working_layout = copy.deepcopy(st.session_state.saved_layouts)
    st.session_state.filters_loaded = True

    with st.sidebar:
    # ... (sidebar code with filters) ...

    if st.button("Save Preferences"):
    # DEBUGGING SHOWS that st.session_state.working_layout has the OLD layout here
    st.write("Layout that would be saved:", st.session_state.working_layout)
    try:
    # This line saves the OLD layout, not the new one
    supabase.table("profiles").update({ "saved_layouts": st.session_state.working_layout }).eq("id", user_info.id).execute()
    # ... (rest of saving logic) ...
    st.success("Preferences saved!")
    except Exception as e:
    st.error(f"Error saving preferences: {e}")

    # ... (Header HTML code) ...

    with elements(f"dashboard_elements_{st.session_state.active_desktop}"):
    desktop_key = st.session_state.active_desktop
    if desktop_key not in st.session_state.working_layout:
    st.session_state.working_layout[desktop_key] = copy.deepcopy(st.session_state.saved_layouts.get(desktop_key, DEFAULT_LAYOUT))

    layout_to_display = st.session_state.working_layout[desktop_key]

    with dashboard.Grid(
    layout_to_display,
    draggableHandle=".draggable-handle",
    rowHeight=10,
    cols={"lg": 96, "md": 96, "sm": 48, "xs": 24, "xxs": 12},
    onChange=handle_layout_change
    ):
    # ... (Code for all 8 mui.Paper cards) ...

    def main():
    initialize_session()
    supabase = init_supabase_client()
    if not supabase: return
    if not st.session_state.auth_user: show_login_screen(supabase)
    else: show_dashboard(supabase)

    if __name__ == "__main__":
    main()

    Continue reading...

Compartilhe esta Página