Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce API to build Single Page Applications (SPAs) #2811

Draft
wants to merge 118 commits into
base: main
Choose a base branch
from

Conversation

Alyxion
Copy link
Contributor

@Alyxion Alyxion commented Apr 3, 2024

NiceGUI is for us at Lechler a really awesome solution and step forward from Streamlit in regards of the visualization of live and streaming data as it puts the dev far more in control of which sub elements and page regions are updated when.

On the other hand it is still lacking three for us very crucial features Streamlit offers:

  • A persistent WebSocket connection for the whole user session (per tab)
  • A data storage possibility per tab to enable the user to create multiple app instances.
  • In-memory storage of complex objects (e.g. pandas data tables)

image

This (still work in progress) pull request tries to resolve at least most of the points above. It shall not yet resolve the situation that a user has an unstable internet connection and thus looses the connection to a server completely and needs to reconnect.

Persistent connection

In a scenario where you want to serve your NiceGUI solution not to hundreds of users there is after a certain point no way around scaling the solution over multiple processes, CPUs or over multiple servers.

If you need to load/keep alive large amounts of data per user w/o involving an external database this requires that the whole user session, even between page changes, is bound to one single process on one specific server. Streamlits uses a SPA approach here thus it creates a WebSockets connection once and all follow-up page and tab changes are just virtual thus changing the URL and browser history in the browser using pushstate but never really loading a new page using GET.

As discussed in the Add app.storage.tab or similar (1308) and in Discord there are several use cases where this is crucial to retain in-memory data on a "per session" basis, see below, which consequently requires that there is such a session in the first place.

Per tab storage

A data storage possibility per tab is cruicial to enable the user to create multiple app instances with different login credentials, configurations and views on a per tab basis. This is on purpose volatile so that user-credentials, critical business data etc. are gone once the browser tab was closed and the connection timed out. This shall match the current behavior of st.session_state.

In-memory storage of complex objects

The possibility to store living, non-JSON compatible objects such as Pandas tables, ML model weights etc. on a "per tab" basis and make them as easy accessible among different pages, global helper classes etc. as currently app.storage.user.

Update: Extracted the app.storage.session feature into a separate pull request 2820

@Alyxion Alyxion changed the title Support for per browser-tab session data and a persistent per-session between page changes Support for per browser-tab session data and a persistent per-session connection between page changes Apr 3, 2024
Michael Ikemann and others added 11 commits April 3, 2024 14:36
Integrated single page app into Client.open so navigation to SPA pages is redirected.
Fixed bug with forward and backwards navigation between SPA pages.
Collecting data from original pages to able to apply the original page title.
Integrated single page app into Client.open so navigation to SPA pages is redirected.
Fixed bug with forward and backwards navigation between SPA pages.
Collecting data from original pages to able to apply the original page title.
Fixed a bug which could occur when open was called before the UI was set up
…s registry in Client.

General clean-up
Added titles to sample app
Added docu to SPA
@Alyxion Alyxion force-pushed the feature/client_data branch from 4b89fb3 to 13f29ac Compare April 3, 2024 12:40
@falkoschindler falkoschindler requested a review from rodja April 3, 2024 14:06
…the structure of the root page, the possibility to override the class and implement custom routing and to react to the creation of sessions.

* Added samples for the single page router
* Refactored the Login sample with the new possibilities and making use of Pydantic as an example for a cleaner code base
@rodja
Copy link
Member

rodja commented Apr 4, 2024

Thank you @Alyxion. There are a lot of interesting things in here. While you are right that a certain kind of applications my want to combine SPA and "per tab storage", it would make review and discussions simpler if you could create two separate pull requests for these features.

…in the current Client instance - which in practice means "per browser tab".
@Alyxion
Copy link
Contributor Author

Alyxion commented Apr 4, 2024

Hello @rodja, sure. As this Pull Request here is anyway just a draft yet and the amount of changes of the per-tab data was manageable I extracted the lines into the separate request #2820 and added a unit test to it. As this one is dependent on the other I will keep it the way it is for now.

@Alyxion
Copy link
Contributor Author

Alyxion commented Apr 5, 2024

Added support for query and URL path parameters such as required by the modularization example.

https://github.com/Alyxion/nicegui/blob/feature/client_data/examples/modularization/main.py works really like a charm now in regards of user experience when switching pages.

At the moment it still throws a "Found top level layout element "Header" inside element "SinglePageRouterFrame". Top level layout elements should not be nested but must be direct children of the page content. This will be raising an exception in NiceGUI 1.5" warning due to the fact that it does not know yet that it actually is in the page content in that case.

Update: Fixed the warning. PageLayout now also accepts the SPA root as valid parent for top level elements.

@Alyxion
Copy link
Contributor Author

Alyxion commented Jun 30, 2024

  • Made use of the Client.request and removed request_data.
  • Merged Outlet and SinglePageRouterConfig
    • As the SinglePageRouterPath was only used by the SinglePageRouterConfig I renamed it accordingly to OutletPath
  • Merged your pull request.
    • It can not be applied to index sites yet ('/'), so when I tried converting the .views to .outlet in the single_page_app_complex demo it did not work anymore b/c it of course assume it's standard behavior and then catches all pages with contain it's name. Will have a look next weekend again.

@nistvan86
Copy link

Hi!
I don't like to derail this thread too much. I've stumbled upon NiceGUI recently, and as part of evaluation I was checking if it could be used for SPA applications, and found this PR which seems to be in a far progressed state.
How much do you expect the API to change still? How usable is the source branch is according to your tests?
I'm only planning some toy project to be based on this, but would be to good to know still if I could start building on top of it.

@Alyxion
Copy link
Contributor Author

Alyxion commented Sep 27, 2024

Hi! I don't like to derail this thread too much. I've stumbled upon NiceGUI recently, and as part of evaluation I was checking if it could be used for SPA applications, and found this PR which seems to be in a far progressed state. How much do you expect the API to change still? How usable is the source branch is according to your tests? I'm only planning some toy project to be based on this, but would be to good to know still if I could start building on top of it.

Hello nistvan and sorry for the very late reply but the non-digital life needs me at the moment at 110%, so the state froze here unfortunately a little bit.

At the end of the days its of course zaubezeugs decision if/when it will find its way into the main branch.

The "only" two major flaws I still see right now is:

  • Documentation / clean up
  • Lacking async support

Other than that there was still a discussion going on if there shall be a clean separation between functions which just provide a template (e.g. ui.outlet or Outlet.outlet), so containing a yield, and functions which contain just the final page. (decorated via outlet.view). Rodja provided a suggestion for merging both in one decorator but that one has issues yet. As for now as we cruicially need the feature internally at my company we maintain a separate repo but hopefully find the time soon to finalize it.

Anyways I just merged all recent changes yesterday again into the PR so its in synch with the newest main branch state again.

@Alyxion
Copy link
Contributor Author

Alyxion commented Oct 20, 2024

  • Added support for async view methods
  • Reverted to strict separation between outlet and view methods again. Outlets are sync and provide the skeleton / layout, views are the dynamic content and can either be sync or async

@Mte90
Copy link

Mte90 commented Nov 20, 2024

any plans for this PR?

@rodja
Copy link
Member

rodja commented Nov 20, 2024

Yes, we really want to merge this. I still need to find the time for another round of review. Especially we need to make a decision about the separation between view and outlet.

@whoamiafterall
Copy link

Hi, i just wanted to state that i'm also really looking forward to this - but dont want to put pressure on you :)

@Alyxion
Copy link
Contributor Author

Alyxion commented Dec 2, 2024

We are actually intensively testing this already with a larger user base in our company since a couple of weeks, overall it seems quite stable. We fond one minor bug (loosing query params when using the back button) which we will fix this week and sync again with the main branch.

Also looking forward to see it branched as it enables some very neat features such as dynamic, volatile per-connection - practically "app instance" - data.

@Mte90
Copy link

Mte90 commented Dec 18, 2024

I was checking this PR that maybe resolve my issue #4119 but I think that the documentation is not good enough as we have a new decorator outlet and other stuff that is not documented in this PR.

@Alyxion
Copy link
Contributor Author

Alyxion commented Dec 19, 2024

Thanks @Mte90 . Actually it is documented since a couple of weeks, see the commit message directly above your comment ;-), though some more detail and refinement would definitely not hurt. But due to r/l reasons very limited time resources. Just run the main.py of the PR to run the docu, you can find it in the Pages & Routing section.

@Mte90
Copy link

Mte90 commented Dec 19, 2024

Thanks!

I am not sure if with this feature it is possible to reference an element per user, at the end I implemented in this way #4119 (comment)

@Alyxion
Copy link
Contributor Author

Alyxion commented Dec 19, 2024

Thanks!

I am not sure if with this feature it is possible to reference an element per user, at the end I implemented in this way #4119 (comment)

Yes, it is. We are actually building a kinda complex SPA on it already with several different screens and many parallel users, no issues. Conceptual its like this:

from nicegui import ui
from pydantic import BaseModel, Field


class User(BaseModel):
    counter: int = Field(0, description="Count of times user clicked the button.")


class HomeScreen:
    def __init__(self, user: User):
        self.user = user

    async def build(self):
        ui.button("Click me").on_click(self.on_inc_button_click)
        ui.link("Go to other page", other_page)

    def on_inc_button_click(self):
        self.user.counter += 1
        ui.notification(f"You have clicked {self.user.counter} times.")


class OtherScreen:
    def __init__(self, user: User):
        self.user = user

    async def build(self):
        ui.label(f"Counter is currently at {self.user.counter}")


@ui.outlet("/")
def main_layout():
    user = User()  # actual code could e.g. try to fetch last session data from REDIS here.
    with ui.header(fixed=True):
        ui.label("Sample App")
    yield {"user": user}


@main_layout.view("/")
async def home(user: User):
    await HomeScreen(user).build()


@main_layout.view("/other")
async def other_page(user: User):
    await OtherScreen(user).build()


ui.run()

The outlet is run per user when he/she connects. There one can create per-user instance data. This data is then passed to individual page builders. On top of that we still synch the data w/ REDIS based upon the tab_id but that would exceed the sample here now.

@Mte90
Copy link

Mte90 commented Dec 20, 2024

Cool so I am waiting for this PR to get approval and in meantime I will use what I developed. In the future we willrefactor for this :-D

Maybe also a guide/doc about how to migrate a normal nicegui app to this new solution can be helpful to understand the differences.

* Added the possibility to also use async functions to be used to define an outlet.
* navigate.to now also supports view targets
* Refactored the complex spa sample to be fully async
* Refactored the auth spa example to be completely sync as counter example.
* Bugfix: The authentication sample triggered a page reload on logout because it redirected to / rather than the login page. It now redirects to the login_view to auto-resolve the URL correctly
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

5 participants