diff --git a/ag_grid_demo/ag_grid_demo/__init__.py b/ag_grid_demo/ag_grid_demo/__init__.py index 0f63252..a1311e3 100644 --- a/ag_grid_demo/ag_grid_demo/__init__.py +++ b/ag_grid_demo/ag_grid_demo/__init__.py @@ -1,2 +1,4 @@ -from . import model_dm as model_dm +from . import model_wrapper_customized as model_wrapper_customized +from . import model_wrapper_simple as model_wrapper_simple +from . import selected_items as selected_items from . import tree as tree diff --git a/ag_grid_demo/ag_grid_demo/ag_grid_demo.py b/ag_grid_demo/ag_grid_demo/ag_grid_demo.py index a5901d7..3021aa7 100644 --- a/ag_grid_demo/ag_grid_demo/ag_grid_demo.py +++ b/ag_grid_demo/ag_grid_demo/ag_grid_demo.py @@ -1,71 +1,27 @@ -"""Welcome to Reflex! This file showcases the custom component in a basic app.""" - import reflex as rx -from reflex_ag_grid import ag_grid -import pandas as pd - - -df = pd.read_csv( - "https://raw.githubusercontent.com/plotly/datasets/master/wind_dataset.csv" -) - -column_defs = [ - ag_grid.column_def(field="direction"), - ag_grid.column_def(field="strength"), - ag_grid.column_def(field="frequency"), -] - -class BasicGridState(rx.State): - selection: list[dict[str, str]] = [] - - -def selected_item(item: dict[str, str]) -> rx.Component: - return rx.card( - rx.data_list.root( - rx.foreach( - item, - lambda kv: rx.data_list.item( - rx.data_list.label(kv[0]), - rx.data_list.value(kv[1]), - ), - ), - ), - ) +from .common import demo, DemoState +@demo( + route="/", + title="AG Grid Demo", + description="A collection of examples using AG Grid in Reflex.", +) def index(): - return rx.hstack( - rx.vstack( - ag_grid( - id="ag_grid_basic_1", - row_data=df.to_dict("records"), - column_defs=column_defs, - row_selection="multiple", - on_selection_changed=lambda rows, _0, _1: BasicGridState.set_selection( - rows - ), - width="50vw", - ), - rx.heading("Other demos"), - rx.link("Simple ModelWrapper", href="/model"), - rx.text( - rx.link("Customized ModelWrapper", href="/model-auth"), - " (Generate data)", - ), - rx.link("Tree (enterprise)", href="/tree"), - ), - rx.vstack( - rx.heading("Selected Items"), - rx.hstack( - rx.foreach( - BasicGridState.selection, - selected_item, + return rx.flex( + rx.foreach( + DemoState.pages, + lambda page: rx.card( + rx.vstack( + rx.link(page.title, href=page.route), + rx.text(page.description), ), - wrap="wrap", + width="300px", ), - max_width="48vw", ), + wrap="wrap", + spacing="3", ) diff --git a/ag_grid_demo/ag_grid_demo/common.py b/ag_grid_demo/ag_grid_demo/common.py new file mode 100644 index 0000000..b08b560 --- /dev/null +++ b/ag_grid_demo/ag_grid_demo/common.py @@ -0,0 +1,128 @@ +"""Common components used by all demo pages.""" + +import dataclasses +from functools import wraps +import inspect +from pathlib import Path +import reflex as rx + + +@dataclasses.dataclass(frozen=True) +class DemoPage: + """A demo page.""" + + route: str + title: str + description: str + + +_DEMO_PAGES: dict[str, DemoPage] = {} + + +class DemoState(rx.State): + """State for the demo pages.""" + + @rx.var(cache=True) + def pages(self) -> list[DemoPage]: + return [p for p in _DEMO_PAGES.values() if p.route != "/"] + + +def demo_dropdown(): + """Dropdown to navigate between demo pages.""" + + return rx.select.root( + rx.select.trigger(placeholder="Select Demo"), + rx.select.content( + rx.foreach( + DemoState.pages, + lambda page: rx.select.item( + rx.heading(page.title, size="3"), value=page.route + ), + ) + ), + value=rx.cond( + rx.State.router.page.path == "/", + "", + rx.State.router.page.path, + ), + on_change=rx.redirect, + ) + + +def demo_template(page): + """Template for all demo pages.""" + + page_data = _DEMO_PAGES[page.__name__] + page_file = Path(inspect.getfile(page)) + page_source = page_file.read_text() + relative_page_file = page_file.relative_to( + Path(__file__, "..", "..", "..", "..").resolve() + ) + + return rx.container( + rx.color_mode.button(position="top-right"), + rx.hstack( + demo_dropdown(), + rx.spacer(), + rx.link( + rx.heading("reflex-ag-grid demo", size="4"), + href="/", + ), + width="100%", + align="center", + ), + rx.text(page_data.description, margin_left="10px", margin_top="5px"), + rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger( + rx.hstack(rx.icon("eye"), rx.text("Example")), value="example" + ), + rx.tabs.trigger( + rx.hstack(rx.icon("code"), rx.text("Source")), value="source" + ), + margin_bottom="1em", + ), + rx.tabs.content(page(), value="example", height="fit-content"), + rx.tabs.content( + rx.card( + rx.inset( + rx.badge( + rx.code(str(relative_page_file)), + width="100%", + size="2", + height="3em", + radius="none", + ), + side="top", + pb="current", + ), + rx.code_block( + page_source, language="python", show_line_numbers=True + ), + ), + value="source", + ), + default_value="example", + margin_top="1em", + ), + rx.logo(), + size="4", + ) + + +def demo(route: str, title: str, description: str, **kwargs): + """Decorator to add the demo page to the demo registry.""" + + def decorator(page): + _DEMO_PAGES[page.__name__] = DemoPage( + route=route, title=title, description=description + ) + + @rx.page(route=route, title=title, description=description, **kwargs) + @wraps(page) + def inner(): + return demo_template(page) + + return inner + + return decorator diff --git a/ag_grid_demo/ag_grid_demo/model_dm.py b/ag_grid_demo/ag_grid_demo/model_wrapper_customized.py similarity index 65% rename from ag_grid_demo/ag_grid_demo/model_dm.py rename to ag_grid_demo/ag_grid_demo/model_wrapper_customized.py index 5a19160..9353ab4 100644 --- a/ag_grid_demo/ag_grid_demo/model_dm.py +++ b/ag_grid_demo/ag_grid_demo/model_wrapper_customized.py @@ -1,57 +1,9 @@ -import datetime - -import faker import reflex as rx -from sqlmodel import Column, DateTime, Field, func - -from reflex_ag_grid.wrapper import model_wrapper, ModelWrapper - - -class Friend(rx.Model, table=True): - name: str - age: int - years_known: int - owes_me: bool = False - has_a_dog: bool = False - spouse_is_annoying: bool = False - met: datetime.datetime = Field( - sa_column=Column(DateTime(timezone=True), server_default=func.now()), - ) - - @classmethod - def generate_fakes(cls, n: int) -> list["Friend"]: - new_friends = [] - fake = faker.Faker() - for _ in range(n): - name = fake.name() - age = fake.random_int(min=18, max=80) - years_known = fake.random_int(min=0, max=age) - new_friends.append( - Friend( - name=name, - age=age, - years_known=years_known, - owes_me=fake.pybool(20), - has_a_dog=fake.pybool(60), - spouse_is_annoying=fake.pybool(30), - met=fake.date_time_between( - start_date=f"-{years_known+1}y", end_date=f"-{years_known}y" - ), - ), - ) - return new_friends +from reflex_ag_grid.wrapper import ModelWrapper -# Basic Example of a ModelWrapper with no customization -@rx.page("/model") -def model_page(): - return rx.box( - model_wrapper( - model_class=Friend, - ), - width="100vw", - height="100vh", - ) +from .common import demo +from .model_wrapper_simple import Friend # This bogus auth state demonstrates how an extended ModelWrapper can check @@ -105,7 +57,7 @@ async def _get_data(self, start, end, filter_model, sort_model): auth_state = await self.get_state(AuthState) if not auth_state.logged_in: return [] # no records for logged out users - return super()._get_data( + return await super()._get_data( start, end, filter_model=filter_model, sort_model=sort_model ) @@ -117,7 +69,11 @@ def selected_items(self) -> list[Friend]: # Advanced example of an extended ModelWrapper with custom behavior -@rx.page("/model-auth") +@demo( + route="/model-auth", + title="Customized ModelWrapper", + description="Extended infinite-row ModelWrapper with custom behavior and auth.", +) def model_page_auth(): grid = FriendModelWrapper.create( model_class=Friend, @@ -141,7 +97,8 @@ def model_page_auth(): ), rx.box( grid, - width="100vw", - height="90vh", + width="100%", + height="65vh", + padding_bottom="60px", # for scroll bar and controls ), ) diff --git a/ag_grid_demo/ag_grid_demo/model_wrapper_simple.py b/ag_grid_demo/ag_grid_demo/model_wrapper_simple.py new file mode 100644 index 0000000..b551bc4 --- /dev/null +++ b/ag_grid_demo/ag_grid_demo/model_wrapper_simple.py @@ -0,0 +1,60 @@ +import datetime + +import faker +import reflex as rx +from sqlmodel import Column, DateTime, Field, func + +from reflex_ag_grid.wrapper import model_wrapper + +from .common import demo + + +class Friend(rx.Model, table=True): + name: str + age: int + years_known: int + owes_me: bool = False + has_a_dog: bool = False + spouse_is_annoying: bool = False + met: datetime.datetime = Field( + sa_column=Column(DateTime(timezone=True), server_default=func.now()), + ) + + @classmethod + def generate_fakes(cls, n: int) -> list["Friend"]: + new_friends = [] + fake = faker.Faker() + for _ in range(n): + name = fake.name() + age = fake.random_int(min=18, max=80) + years_known = fake.random_int(min=0, max=age) + new_friends.append( + Friend( + name=name, + age=age, + years_known=years_known, + owes_me=fake.pybool(20), + has_a_dog=fake.pybool(60), + spouse_is_annoying=fake.pybool(30), + met=fake.date_time_between( + start_date=f"-{years_known+1}y", end_date=f"-{years_known}y" + ), + ), + ) + return new_friends + + +@demo( + route="/model", + title="Simple ModelWrapper", + description="Basic example of an infinite-row ModelWrapper with no customization.", +) +def model_page(): + return rx.box( + model_wrapper( + model_class=Friend, + ), + width="100%", + height="71vh", + padding_bottom="60px", # for scroll bar and controls + ) diff --git a/ag_grid_demo/ag_grid_demo/selected_items.py b/ag_grid_demo/ag_grid_demo/selected_items.py new file mode 100644 index 0000000..08e6063 --- /dev/null +++ b/ag_grid_demo/ag_grid_demo/selected_items.py @@ -0,0 +1,72 @@ +import reflex as rx +from reflex_ag_grid import ag_grid +import pandas as pd + +from .common import demo + + +df = pd.read_csv( + "https://raw.githubusercontent.com/plotly/datasets/master/wind_dataset.csv" +) + +column_defs = [ + ag_grid.column_def(field="direction"), + ag_grid.column_def(field="strength"), + ag_grid.column_def(field="frequency"), +] + + +class BasicGridState(rx.State): + selection: list[dict[str, str]] = [] + + +def selected_item(item: dict[str, str]) -> rx.Component: + return rx.card( + rx.data_list.root( + rx.foreach( + item, + lambda kv: rx.data_list.item( + rx.data_list.label(kv[0]), + rx.data_list.value(kv[1]), + ), + ), + ), + ) + + +@demo( + route="/selected-items", + title="Selected Items", + description="Handle grid selection events and display selected items in a separate panel.", +) +def selected_items_example(): + return rx.hstack( + ag_grid( + id="ag_grid_basic_1", + row_data=df.to_dict("records"), + column_defs=column_defs, + row_selection="multiple", + on_selection_changed=lambda rows, _0, _1: BasicGridState.set_selection( + rows + ), + width="50%", + height="71vh", + ), + rx.vstack( + rx.heading( + f"Selected Items ({BasicGridState.selection.length()})", size="4" + ), + rx.scroll_area( + rx.hstack( + rx.foreach( + BasicGridState.selection, + selected_item, + ), + wrap="wrap", + ), + ), + max_width="48%", + height="71vh", + ), + width="100%", + ) diff --git a/ag_grid_demo/ag_grid_demo/tree.py b/ag_grid_demo/ag_grid_demo/tree.py index c535c8e..5c273c0 100644 --- a/ag_grid_demo/ag_grid_demo/tree.py +++ b/ag_grid_demo/ag_grid_demo/tree.py @@ -3,6 +3,8 @@ from reflex_ag_grid import ag_grid +from .common import demo + human_size = rx.vars.FunctionStringVar(""" (params) => { @@ -204,7 +206,11 @@ class TreeDisplayState(rx.State): combine_hosts: rx.Field[bool] = rx.field(True) -@rx.page("/tree") +@demo( + route="/tree", + title="Tree (enterprise)", + description="Use tree data with get_data_path to visualize hierarchical information.", +) def tree_example(): return rx.box( rx.hstack( @@ -256,6 +262,7 @@ def tree_example(): # This key causes the grid to re-initialize when `combine_hosts` var changes key=f"ag_grid_{TreeDisplayState.combine_hosts}", ), - width="100vw", - height="100vh", + width="100%", + height="71vh", + padding_bottom="25px", # for scroll bar )