Skip to content

Commit

Permalink
fix merge conflict
Browse files Browse the repository at this point in the history
  • Loading branch information
AkshatJawne committed Nov 8, 2024
2 parents e7ecbf1 + 2eb5fab commit 45f92fc
Show file tree
Hide file tree
Showing 66 changed files with 2,681 additions and 849 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test-python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ jobs:
working-directory: 'plugins/${{ inputs.package }}'

- name: Run tox
run: tox -e py
run: tox -e py${{ matrix.python }}
working-directory: 'plugins/${{ inputs.package }}'
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,30 @@ You should be able to pass arguments to these commands as if you were running Pl
It is highly recommended to use `npm run e2e:docker` (instead of `npm run e2e`) as CI also uses the same environment. You can also use `npm run e2e:update-snapshots` to regenerate snapshots in said environment. Run Playwright in [UI Mode](https://playwright.dev/docs/test-ui-mode) with `npm run e2e:ui` when creating new tests or debugging, as this will allow you to run each test individually, see the browser as it runs it, inspect the console, evaluate locators, etc.

### Running Python tests

The above steps will also set up `tox` to run tests for the python plugins that support it.
The [venv setup](#pre-commit-hookspython-formatting) steps will also set up `tox` to run tests for the python plugins that support it.
Note that `tox` sets up an isolated environment for running tests.
Be default, `tox` will run against Python 3.8, which will need to be installed on your system before running tests.
You can run tests with the following command from the `plugins/<plugin>` directory:

```shell
tox -e py
```

> [!IMPORTANT]
> Linux, and possibly other setups such as MacOS depending on method, may require additional packages to be installed to run Python 3.8.
> ```shell
> sudo apt install python3.8 python3.8-distutils libpython3.8
> # or just full install although it will include more packages than necessary
> sudo apt install python3.8-full
> ```

You can also run tests against a specific version of python by appending the version to `py`
This assumes that the version of Python you're targeting is installed on your system.
For example, to run tests against Python 3.12, run:
```shell
tox -e py3.12
```
### Running plugin against deephaven-core
#### Building Python plugins for development
Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 19 additions & 1 deletion plugins/plotly-express/tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,22 @@ isolated_build = True
deps =
deephaven-server
commands =
python -m unittest discover
python -m unittest discover
basepython = python3.8

[testenv:py3.8]
basepython = python3.8

[testenv:py3.9]
basepython = python3.9

[testenv:py3.10]
basepython = python3.10

[testenv:py3.11]
basepython = python3.11

[testenv:py3.12]
basepython = python3.12


106 changes: 0 additions & 106 deletions plugins/ui/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -3542,112 +3542,6 @@ With callbacks, there will be a delay between when the user makes changes in the
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.
#### Architecture
##### Rendering
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.
Let's say we execute the following, where a table is filtered based on the value of a text input:
```python
from deephaven import ui
@ui.component
def text_filter_table(source, column, initial_value=""):
value, set_value = ui.use_state(initial_value)
ti = ui.text_field(value=value, on_change=set_value)
tt = source.where(f"{column}=`{value}`")
return [ti, tt]
# This will render two panels, one filtering the table by Sym, and the other by Exchange
@ui.component
def double_text_filter_table(source):
tft1 = text_filter_table(source, "Sym")
tft2 = text_filter_table(source, "Exchange")
return ui.panel(tft1, title="Sym"), ui.panel(tft2, title="Exchange")
import deephaven.plot.express as dx
_stocks = dx.data.stocks()
tft = double_text_filter_table(_stocks)
```
Which should result in a UI like this:
![Double Text Filter Tables](docs/_assets/double-tft.png)
How does that look when the notebook is executed? When does each code block execute?
```mermaid
sequenceDiagram
participant U as User
participant W as Web UI
participant UIP as UI Plugin
participant C as Core
participant SP as Server Plugin
U->>W: Run notebook
W->>C: Execute code
C->>SP: is_type(object)
SP-->>C: Matching plugin
C-->>W: VariableChanges(added=[t, tft])
W->>UIP: Open tft
UIP->>C: Export tft
C-->>UIP: tft (Element)
Note over UIP: UI knows about object tft<br/>double_text_filter_table not executed yet
UIP->>SP: Render tft (initialState)
SP->>SP: Run double_text_filter_table
Note over SP: double_text_filter_table executes, running text_filter_table twice
SP-->>UIP: Result (document=[panel(tft1), pane(tft2)], exported_objects=[tft1, tft2])
UIP-->>W: Display Result
U->>UIP: Change text input 1
UIP->>SP: Change state
SP->>SP: Run double_text_filter_table
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
SP-->>UIP: Result (document=[panel(tft1'), panel(tft2)], state={}, exported_objects=[tft1'])
UIP-->>W: Display Result
```
##### Communication/Callbacks
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:
```mermaid
sequenceDiagram
participant UIP as UI Plugin
participant SP as Server Plugin
Note over UIP, SP: Uses JSON-RPC
UIP->>SP: setState(initialState)
SP-->>UIP: documentUpdated(Document, State)
loop Callback
UIP->>SP: foo(params)
SP-->>UIP: foo result
SP->>UIP: documentUpdated(Document, State)
Note over UIP: Client can store State to restore the same state later
end
```
##### Communication Layers
A component that is created on the server side runs through a few steps before it is rendered on the client side:
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).
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`.
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.
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.
5. [DocumentHandler](./src/js/src/DocumentHandler.tsx) - Handles the root of a rendered document, laying out the appropriate panels or dashboard specified.
#### Other Decisions
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.
Expand Down
2 changes: 2 additions & 0 deletions plugins/ui/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
# options for sphinx_autodoc_typehints
always_use_bars_union = True

strip_signature_backslash = True

from deephaven_server import Server

# need a server instance to pull types from the autodocs
Expand Down
4 changes: 2 additions & 2 deletions plugins/ui/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ my_button = ui.button("Click Me!", on_press=lambda e: print(f"Button was clicked

## Creating components

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]().
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).

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

Expand Down Expand Up @@ -893,7 +893,7 @@ def ui_range_table(source, column):
my_range_table = ui_range_table(stocks, "Size")
```

![Table with a slider for selecting the range.](_assets/range_table.png)
![Table with a slider for selecting the range.](./_assets/range_table.png)

## Table with required filters

Expand Down
149 changes: 149 additions & 0 deletions plugins/ui/docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Architecture

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.

## Components

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.

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.

```python
from deephaven import ui


@ui.component
def my_button():
return ui.button("Click me!")


btn = my_button()
```

Once you have declared a component, you can nest it into another component.

```python
@ui.component
def my_app():
return ui.flex(ui.text("Hello, world!"), my_button(), direction="column")


app = my_app()
```

## Rendering

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.

Let's say we execute the following, where a table is filtered based on the value of a text input:

```python
from deephaven import ui
import deephaven.plot.express as dx


@ui.component
def text_filter_table(source, column, initial_value=""):
value, set_value = ui.use_state(initial_value)
ti = ui.text_field(value=value, on_change=set_value)
tt = source.where(f"{column}=`{value}`")
return [ti, tt]


# This will render two panels, one filtering the table by Sym, and the other by Exchange
@ui.component
def double_text_filter_table(source):
tft1 = text_filter_table(source, "Sym", "CAT")
tft2 = text_filter_table(source, "Exchange", "PETX")
return ui.panel(tft1, title="Sym"), ui.panel(tft2, title="Exchange")


_stocks = dx.data.stocks()

tft = double_text_filter_table(_stocks)
```

This should result in a UI like:

![Double Text Filter Tables](_assets/double-tft.png)

How does that look when the notebook is executed? When does each code block execute?

```mermaid
sequenceDiagram
participant U as User
participant W as Web UI
participant UIP as UI Plugin
participant C as Core
participant SP as Server Plugin
U->>W: Run notebook
W->>C: Execute code
C->>SP: is_type(object)
SP-->>C: Matching plugin
C-->>W: VariableChanges(added=[t, tft])
W->>UIP: Open tft
activate UIP
UIP->>C: Fetch tft
C-->>UIP: Export tft (Element)
Note over UIP: UI knows about object tft<br/>double_text_filter_table not executed yet
UIP->>SP: Render tft (initialState)
SP->>SP: Run double_text_filter_table
Note over SP: double_text_filter_table executes, running text_filter_table twice
SP-->>UIP: Result (document=[panel(tft1), pane(tft2)], exported_objects=[tft1, tft2])
UIP-->>W: Display Result
deactivate UIP
U->>UIP: Change text input 1
activate UIP
UIP->>SP: Change state
SP->>SP: Run double_text_filter_table
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
SP-->>UIP: Result (document=[panel(tft1'), panel(tft2)], <br/>state={}, exported_objects=[tft1'])
UIP-->>W: Display Result
deactivate UIP
```

### Threads and rendering

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.

### Render context

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.

## Communication/Callbacks

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:

```mermaid
sequenceDiagram
participant UIP as UI Plugin
participant SP as Server Plugin
Note over UIP, SP: Uses JSON-RPC
UIP->>SP: setState(initialState)
SP-->>UIP: documentUpdated(Document, State)
loop Callback
UIP->>SP: foo(params)
SP-->>UIP: foo result
opt Update sent if callback modified state
SP->>UIP: documentUpdated(Document, State)
end
Note over UIP: Client can store State to restore the same state later
end
```

## Communication Layers

A component that is created on the server side runs through a few steps before it is rendered on the client side:

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).
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`.
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.
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.
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.
Loading

0 comments on commit 45f92fc

Please sign in to comment.