Skip to content

Commit 6ae6493

Browse files
mofojedmargaretkennedymattrunyon
authored
docs: Add architecture document (#949)
- Provide details about the overall high-level architecture of deephaven.ui - Diagram a couple work flows - Fixes #954 --------- Co-authored-by: margaretkennedy <[email protected]> Co-authored-by: Matthew Runyon <[email protected]>
1 parent bbe18e3 commit 6ae6493

File tree

4 files changed

+154
-107
lines changed

4 files changed

+154
-107
lines changed

plugins/ui/DESIGN.md

Lines changed: 0 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -3542,112 +3542,6 @@ With callbacks, there will be a delay between when the user makes changes in the
35423542
35433543
The above examples are all in Python, and particularly take some advantage of language constructs in python (such as positional arguments and kwargs). We should consider how it would work in Groovy/Java as well, and how we can build one on top of the other.
35443544
3545-
#### Architecture
3546-
3547-
##### Rendering
3548-
3549-
When you call a function decorated by `@ui.component`, it will return an `Element` object that has a reference to the function it is decorated; that is to say, the function does _not_ get run immediately. The function is only run when the `Element` is rendered by the client, and the result is sent back to the client. This allows the `@ui.component` decorator to execute the function with the appropriate rendering context. The client must also set the initial state before rendering, allowing the client to persist the state and re-render in the future.
3550-
3551-
Let's say we execute the following, where a table is filtered based on the value of a text input:
3552-
3553-
```python
3554-
from deephaven import ui
3555-
3556-
3557-
@ui.component
3558-
def text_filter_table(source, column, initial_value=""):
3559-
value, set_value = ui.use_state(initial_value)
3560-
ti = ui.text_field(value=value, on_change=set_value)
3561-
tt = source.where(f"{column}=`{value}`")
3562-
return [ti, tt]
3563-
3564-
3565-
# This will render two panels, one filtering the table by Sym, and the other by Exchange
3566-
@ui.component
3567-
def double_text_filter_table(source):
3568-
tft1 = text_filter_table(source, "Sym")
3569-
tft2 = text_filter_table(source, "Exchange")
3570-
return ui.panel(tft1, title="Sym"), ui.panel(tft2, title="Exchange")
3571-
3572-
3573-
import deephaven.plot.express as dx
3574-
3575-
_stocks = dx.data.stocks()
3576-
3577-
tft = double_text_filter_table(_stocks)
3578-
```
3579-
3580-
Which should result in a UI like this:
3581-
3582-
![Double Text Filter Tables](docs/_assets/double-tft.png)
3583-
3584-
How does that look when the notebook is executed? When does each code block execute?
3585-
3586-
```mermaid
3587-
sequenceDiagram
3588-
participant U as User
3589-
participant W as Web UI
3590-
participant UIP as UI Plugin
3591-
participant C as Core
3592-
participant SP as Server Plugin
3593-
3594-
U->>W: Run notebook
3595-
W->>C: Execute code
3596-
C->>SP: is_type(object)
3597-
SP-->>C: Matching plugin
3598-
C-->>W: VariableChanges(added=[t, tft])
3599-
3600-
W->>UIP: Open tft
3601-
UIP->>C: Export tft
3602-
C-->>UIP: tft (Element)
3603-
3604-
Note over UIP: UI knows about object tft<br/>double_text_filter_table not executed yet
3605-
3606-
UIP->>SP: Render tft (initialState)
3607-
SP->>SP: Run double_text_filter_table
3608-
Note over SP: double_text_filter_table executes, running text_filter_table twice
3609-
SP-->>UIP: Result (document=[panel(tft1), pane(tft2)], exported_objects=[tft1, tft2])
3610-
UIP-->>W: Display Result
3611-
3612-
U->>UIP: Change text input 1
3613-
UIP->>SP: Change state
3614-
SP->>SP: Run double_text_filter_table
3615-
Note over SP: double_text_filter_table executes, text_filter_table only <br/>runs once for the one changed input<br/>only exports the new table, as client already has previous tables
3616-
SP-->>UIP: Result (document=[panel(tft1'), panel(tft2)], state={}, exported_objects=[tft1'])
3617-
UIP-->>W: Display Result
3618-
```
3619-
3620-
##### Communication/Callbacks
3621-
3622-
When the document is first rendered, it will pass the entire document to the client. When the client makes a callback, it needs to send a message to the server indicating which callback it wants to trigger, and with which parameters. For this, we use [JSON-RPC](https://www.jsonrpc.org/specification). When the client opens the message stream to the server, the communication looks like:
3623-
3624-
```mermaid
3625-
sequenceDiagram
3626-
participant UIP as UI Plugin
3627-
participant SP as Server Plugin
3628-
3629-
Note over UIP, SP: Uses JSON-RPC
3630-
UIP->>SP: setState(initialState)
3631-
SP-->>UIP: documentUpdated(Document, State)
3632-
3633-
loop Callback
3634-
UIP->>SP: foo(params)
3635-
SP-->>UIP: foo result
3636-
SP->>UIP: documentUpdated(Document, State)
3637-
Note over UIP: Client can store State to restore the same state later
3638-
end
3639-
```
3640-
3641-
##### Communication Layers
3642-
3643-
A component that is created on the server side runs through a few steps before it is rendered on the client side:
3644-
3645-
1. [Element](./src/deephaven/ui/elements/Element.py) - The basis for all UI components. Generally a [FunctionElement](./src/deephaven/ui/elements/FunctionElement.py) created by a script using the [@ui.component](./src/deephaven/ui/components/make_component.py) decorator, and does not run the function until it is rendered. The result can change depending on the context that it is rendered in (e.g. what "state" is set).
3646-
2. [ElementMessageStream](./src/deephaven/ui/object_types/ElementMessageStream.py) - The `ElementMessageStream` is responsible for rendering one instance of an element in a specific rendering context and handling the server-client communication. The element is rendered to create a [RenderedNode](./src/deephaven/ui/renderer/RenderedNode.py), which is an immutable representation of a rendered document. The `RenderedNode` is then encoded into JSON using [NodeEncoder](./src/deephaven/ui/renderer/NodeEncoder.py), which pulls out all the non-serializable objects (such as Tables) and maps them to exported objects, and all the callables to be mapped to commands that can be accepted by JSON-RPC. This is the final representation of the document that is sent to the client, and ultimately handled by the `WidgetHandler`.
3647-
3. [DashboardPlugin](./src/js/src/DashboardPlugin.tsx) - Client side `DashboardPlugin` that listens for when a widget of type `Element` is opened, and manage the `WidgetHandler` instances that are created for each widget.
3648-
4. [WidgetHandler](./src/js/src/WidgetHandler.tsx) - Uses JSON-RPC communication with an `ElementMessageStream` instance to load the initial rendered document and associated exported objects. Listens for any changes and updates the document accordingly.
3649-
5. [DocumentHandler](./src/js/src/DocumentHandler.tsx) - Handles the root of a rendered document, laying out the appropriate panels or dashboard specified.
3650-
36513545
#### Other Decisions
36523546
36533547
While mocking this up, there are a few decisions regarding the syntax we should be thinking about/address prior to getting too far along with implementation.

plugins/ui/docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ my_button = ui.button("Click Me!", on_press=lambda e: print(f"Button was clicked
5858

5959
## Creating components
6060

61-
Use the `@ui.component` decorator to create your own custom components. This decorator wraps the function provided as a Deephaven component. For more details on the architecture, see [TODO: Link to architecture]().
61+
Use the `@ui.component` decorator to create your own custom components. This decorator wraps the function provided as a Deephaven component. For more details on the architecture, see [Architecture documentation](./architecture.md).
6262

6363
We can display a heading above a button as our custom component:
6464

plugins/ui/docs/architecture.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Architecture
2+
3+
deephaven.ui is a flexible and extensible [React-like](https://react.dev/learn/thinking-in-react) UI framework that can create complex UIs in Python. You can create UIs using only the components provided by deephaven.ui, or you can create your own components using the `@ui.component` decorator.
4+
5+
## Components
6+
7+
Components are reusable pieces of UI that can be combined to create complex UIs. Each component defines its own logic and appearance. Components can be simple, like a button, or complex, like a table with controls for filtering and sorting. Components can also be composed of other components, allowing for complex UIs to be built up from simpler pieces.
8+
9+
Components are created using the `@ui.component` decorator. This decorator takes a function that returns a list of components, and returns a new function that can be called to render the component. The function returned by the decorator is called a "component function". Calling the function and assigning it to a variable will create an "element" that can be rendered by the client.
10+
11+
```python
12+
from deephaven import ui
13+
14+
15+
@ui.component
16+
def my_button():
17+
return ui.button("Click me!")
18+
19+
20+
btn = my_button()
21+
```
22+
23+
Once you have declared a component, you can nest it into another component.
24+
25+
```python
26+
@ui.component
27+
def my_app():
28+
return ui.flex(ui.text("Hello, world!"), my_button(), direction="column")
29+
30+
31+
app = my_app()
32+
```
33+
34+
## Rendering
35+
36+
When you call a function decorated by `@ui.component`, it will return an `Element` object that references the function it is decorated by; that is to say, the function does _not_ run immediately. The function runs when the `Element` is rendered by the client, and the result is sent back to the client. This allows the `@ui.component` decorator to execute the function with the appropriate rendering context. The client must also set the initial state before rendering, allowing the client to persist the state and re-render in the future.
37+
38+
Let's say we execute the following, where a table is filtered based on the value of a text input:
39+
40+
```python
41+
from deephaven import ui
42+
import deephaven.plot.express as dx
43+
44+
45+
@ui.component
46+
def text_filter_table(source, column, initial_value=""):
47+
value, set_value = ui.use_state(initial_value)
48+
ti = ui.text_field(value=value, on_change=set_value)
49+
tt = source.where(f"{column}=`{value}`")
50+
return [ti, tt]
51+
52+
53+
# This will render two panels, one filtering the table by Sym, and the other by Exchange
54+
@ui.component
55+
def double_text_filter_table(source):
56+
tft1 = text_filter_table(source, "Sym", "CAT")
57+
tft2 = text_filter_table(source, "Exchange", "PETX")
58+
return ui.panel(tft1, title="Sym"), ui.panel(tft2, title="Exchange")
59+
60+
61+
_stocks = dx.data.stocks()
62+
63+
tft = double_text_filter_table(_stocks)
64+
```
65+
66+
This should result in a UI like:
67+
68+
![Double Text Filter Tables](_assets/double-tft.png)
69+
70+
How does that look when the notebook is executed? When does each code block execute?
71+
72+
```mermaid
73+
sequenceDiagram
74+
participant U as User
75+
participant W as Web UI
76+
participant UIP as UI Plugin
77+
participant C as Core
78+
participant SP as Server Plugin
79+
80+
U->>W: Run notebook
81+
W->>C: Execute code
82+
C->>SP: is_type(object)
83+
SP-->>C: Matching plugin
84+
C-->>W: VariableChanges(added=[t, tft])
85+
86+
W->>UIP: Open tft
87+
activate UIP
88+
UIP->>C: Fetch tft
89+
C-->>UIP: Export tft (Element)
90+
91+
Note over UIP: UI knows about object tft<br/>double_text_filter_table not executed yet
92+
93+
UIP->>SP: Render tft (initialState)
94+
SP->>SP: Run double_text_filter_table
95+
Note over SP: double_text_filter_table executes, running text_filter_table twice
96+
SP-->>UIP: Result (document=[panel(tft1), pane(tft2)], exported_objects=[tft1, tft2])
97+
UIP-->>W: Display Result
98+
deactivate UIP
99+
100+
U->>UIP: Change text input 1
101+
activate UIP
102+
UIP->>SP: Change state
103+
SP->>SP: Run double_text_filter_table
104+
Note over SP: double_text_filter_table executes, text_filter_table only <br/>runs once for the one changed input<br/>only exports the new table, as client already has previous tables
105+
SP-->>UIP: Result (document=[panel(tft1'), panel(tft2)], <br/>state={}, exported_objects=[tft1'])
106+
UIP-->>W: Display Result
107+
deactivate UIP
108+
```
109+
110+
### Threads and rendering
111+
112+
When a component is rendered, the render task is [submitted to the Deephaven server as a "concurrent" task](https://deephaven.io/core/pydoc/code/deephaven.server.executors.html#deephaven.server.executors.submit_task). This ensures that rendering one component does not block another component from rendering. A lock is then held on that component instance to ensure it can only be rendered by one thread at a time. After the lock is acquired, a root [render context](#render-context) is set in the thread-local data, and the component is rendered.
113+
114+
### Render context
115+
116+
Each component renders in its own render context, which helps keep track of state and side effects. While rendering components, "hooks" are used to manage state and other side effects. The magic part of hooks is they work based on the order they are called within a component. When a component is rendered, a new context is set, replacing the existing context. When the component is done rendering, the context is reset to the previous context. This allows for nested components to have their own state and side effects, and for the parent component to manage the state of the child components, re-using the same context when re-rendering a child component.
117+
118+
## Communication/Callbacks
119+
120+
When the document is first rendered, it will pass the entire document to the client. When the client makes a callback, it needs to send a message to the server indicating which callback it wants to trigger, and with which parameters. For this, we use [JSON-RPC](https://www.jsonrpc.org/specification). When the client opens the message stream to the server, the communication looks like:
121+
122+
```mermaid
123+
sequenceDiagram
124+
participant UIP as UI Plugin
125+
participant SP as Server Plugin
126+
127+
Note over UIP, SP: Uses JSON-RPC
128+
UIP->>SP: setState(initialState)
129+
SP-->>UIP: documentUpdated(Document, State)
130+
131+
loop Callback
132+
UIP->>SP: foo(params)
133+
SP-->>UIP: foo result
134+
opt Update sent if callback modified state
135+
SP->>UIP: documentUpdated(Document, State)
136+
end
137+
Note over UIP: Client can store State to restore the same state later
138+
end
139+
```
140+
141+
## Communication Layers
142+
143+
A component that is created on the server side runs through a few steps before it is rendered on the client side:
144+
145+
1. [Element](https://github.com/deephaven/deephaven-plugins/blob/main/plugins/ui/src/deephaven/ui/elements/Element.py) - The basis for all UI components. Generally, a [FunctionElement](https://github.com/deephaven/deephaven-plugins/blob/main/plugins/ui/src/deephaven/ui/elements/FunctionElement.py) created by a script using the [@ui.component](https://github.com/deephaven/deephaven-plugins/blob/main/plugins/ui/src/deephaven/ui/components/make_component.py) decorator that does not run the function until it is rendered. The result can change depending on the context that it is rendered in (e.g., what "state" is set).
146+
2. [ElementMessageStream](https://github.com/deephaven/deephaven-plugins/blob/main/plugins/ui/src/deephaven/ui/object_types/ElementMessageStream.py) - The `ElementMessageStream` is responsible for rendering one instance of an element in a specific rendering context and handling the server-client communication. The element is rendered to create a [RenderedNode](https://github.com/deephaven/deephaven-plugins/blob/main/plugins/ui/src/deephaven/ui/renderer/RenderedNode.py), which is an immutable representation of a rendered document. The `RenderedNode` is then encoded into JSON using [NodeEncoder](https://github.com/deephaven/deephaven-plugins/blob/main/plugins/ui/src/deephaven/ui/renderer/NodeEncoder.py), which pulls out all the non-serializable objects (such as Tables) and maps them to exported objects, and all the callables to be mapped to commands that JSON-RPC can accept. This is the final representation of the document sent to the client and ultimately handled by the `WidgetHandler`.
147+
3. [DashboardPlugin](https://github.com/deephaven/deephaven-plugins/blob/main/plugins/ui/src/js/src/DashboardPlugin.tsx) - Client-side `DashboardPlugin` that listens for when a widget of type `Element` is opened and manages the `WidgetHandler` instances that are created for each widget.
148+
4. [WidgetHandler](https://github.com/deephaven/deephaven-plugins/blob/main/plugins/ui/src/js/src/WidgetHandler.tsx) - Uses JSON-RPC communication with an `ElementMessageStream` instance to set the initial state, then load the initial rendered document and associated exported objects. Listens for any changes and updates the document accordingly.
149+
5. [DocumentHandler](https://github.com/deephaven/deephaven-plugins/blob/main/plugins/ui/src/js/src/DocumentHandler.tsx) - Handles the root of a rendered document, laying out the appropriate panels or dashboard specified.

plugins/ui/docs/sidebar.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
"label": "Installation",
1919
"path": "installation.md"
2020
},
21+
{
22+
"label": "Architecture",
23+
"path": "architecture.md"
24+
},
2125
{
2226
"label": "Components",
2327
"items": [

0 commit comments

Comments
 (0)