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

feat: Toast Implementation #1030

Merged
merged 44 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
a52d49f
Toast UI Component Docs
dgodinez-dh Nov 6, 2024
3d5279f
progrmatic dismisal
dgodinez-dh Nov 6, 2024
832dcca
remove todo
dgodinez-dh Nov 8, 2024
c599781
merge latest
dgodinez-dh Nov 8, 2024
e89a4b2
merge latest
dgodinez-dh Nov 14, 2024
31195ea
update docs for simpler syntax
dgodinez-dh Nov 14, 2024
250be22
update deephaven dependencies to v99
dgodinez-dh Nov 18, 2024
69bb8d9
merge latest
dgodinez-dh Nov 18, 2024
51a0cc7
initial toast and event wiring
dgodinez-dh Nov 18, 2024
2c10e3b
add component annotation
dgodinez-dh Nov 18, 2024
f0ae834
js toast event
dgodinez-dh Nov 18, 2024
a0b7424
add variant
dgodinez-dh Nov 19, 2024
38f5745
add all options and camel case
dgodinez-dh Nov 19, 2024
8b6b3ed
use locals
dgodinez-dh Nov 19, 2024
bde4eef
add comments
dgodinez-dh Nov 19, 2024
f36d701
wrap callables
dgodinez-dh Nov 19, 2024
7cf3cfc
only add new callables
dgodinez-dh Nov 19, 2024
3c38df2
show flag
dgodinez-dh Nov 20, 2024
c2feda1
toast variant is not optional
dgodinez-dh Nov 20, 2024
d95a01f
update docs
dgodinez-dh Nov 20, 2024
a1db0a0
fix merge
dgodinez-dh Nov 20, 2024
4ae3b95
sidebar
dgodinez-dh Nov 20, 2024
b05bcf3
fix python unit tests
dgodinez-dh Nov 20, 2024
2548bb9
fix toast
dgodinez-dh Nov 20, 2024
378ec97
update jest config
dgodinez-dh Nov 20, 2024
7ef4955
EventContext
dgodinez-dh Nov 21, 2024
2345dcf
update hook
dgodinez-dh Nov 21, 2024
ce6725e
fix toast
dgodinez-dh Nov 21, 2024
8f5c6e8
hook up new context
dgodinez-dh Nov 21, 2024
6ff2e1a
revert tests
dgodinez-dh Nov 21, 2024
3f47f1f
fix context manager open
dgodinez-dh Nov 21, 2024
a58a472
optional old context
dgodinez-dh Nov 21, 2024
cd52ae7
fix context
dgodinez-dh Nov 21, 2024
67fbd93
toast return doc
dgodinez-dh Nov 22, 2024
4662f7e
update callable code
dgodinez-dh Nov 22, 2024
2b5af04
doc updates
dgodinez-dh Nov 22, 2024
490334e
rename to event_context
dgodinez-dh Nov 22, 2024
6db6a76
update doc
dgodinez-dh Nov 22, 2024
7ed585e
fix merge conflict
dgodinez-dh Nov 25, 2024
de10f5c
fix package lock
dgodinez-dh Nov 25, 2024
44ac86f
use table listener
dgodinez-dh Nov 25, 2024
518e62a
Update plugins/ui/src/js/src/widget/WidgetHandler.tsx
dgodinez-dh Nov 25, 2024
38c10b4
event encoder
dgodinez-dh Nov 26, 2024
556d1c4
fix param type
dgodinez-dh Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions plugins/ui/docs/components/toast.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# Toast

Toasts display brief, temporary notifications of actions, errors, or other events in an application.

## Example

```python
from deephaven import ui

btn = ui.button(
"Show toast",
on_press=lambda: ui.toast("Toast is done!"),
variant="primary",
)
```

## Content

Toasts are triggered using the method `ui.toast`. Toasts use `variant` to specify the following styles: `neutral`, `positive`, `negative`, and `info`. Toast will default to `neutral` if `variant` is omitted.

Toasts are shown according to the order they are added, with the most recent toast appearing at the bottom of the stack. Please use Toasts sparingly.

```python
from deephaven import ui

toasts = ui.button_group(
ui.button(
"Show neutral toast",
on_press=lambda: ui.toast("Toast available", variant="neutral"),
variant="secondary",
),
ui.button(
"Show positive toast",
on_press=lambda: ui.toast("Toast is done!", variant="positive"),
variant="primary",
),
ui.button(
"Show negative toast",
on_press=lambda: ui.toast("Toast is burned!", variant="negative"),
variant="negative",
),
ui.button(
"Show info toast",
on_press=lambda: ui.toast("Toasting...", variant="info"),
variant="accent",
style="outline",
),
)
```

## Events

Toasts can include an optional action by specifying the `action_label` and `on_action` options when queueing a toast. In addition, the `on_close` event is triggered when the toast is dismissed. The `should_close_on_action` option automatically closes the toast when an action is performed.

```python
from deephaven import ui


btn = ui.button(
"Show toast",
on_press=lambda: ui.toast(
"An update is available",
action_label="Update",
on_action=lambda: print("Updating!"),
mofojed marked this conversation as resolved.
Show resolved Hide resolved
should_close_on_action=True,
on_close=lambda: print("Closed"),
variant="positive",
),
variant="primary",
)
```

## Auto-dismiss

Toasts support a `timeout` option to automatically hide them after a certain amount of time. For accessibility, toasts have a minimum `timeout` of 5 seconds, and actionable toasts will not auto dismiss. In addition, timers will pause when the user focuses or hovers over a toast.

Be sure only to automatically dismiss toasts when the information is not important, or may be found elsewhere. Some users may require additional time to read a toast message, and screen zoom users may miss toasts entirely.

```python
from deephaven import ui


btn = ui.button(
"Show toast",
on_press=lambda: ui.toast("Toast is done!", timeout=5000, variant="positive"),
variant="primary",
)
```

## Show toast on mount

This example shows how to display a toast when a component mounts.

```python
from deephaven import ui


@ui.component
def ui_toast_on_mount():
ui.toast("Mounted.", variant="info")
return ui.heading("Toast was shown on mount.")


my_mount_example = ui_toast_on_mount()
```

## Toast from table example

This example shows how to create a toast from the latest update of a ticking table. It is recommended to auto dismiss these toasts with a `timeout` and to avoid ticking faster than the value of the `timeout`.

```python
from deephaven import time_table
from deephaven import ui

_source = time_table("PT5S").update("X = i").tail(5)


@ui.component
def toast_table(t):
render_queue = ui.use_render_queue()

def listener_function(update, is_replay):
data_added = update.added()["X"][0]
render_queue(lambda: ui.toast(f"added {data_added}", timeout=5000))

ui.use_table_listener(t, listener_function, [t])
return t


my_toast_table = toast_table(_source)
```

# Multi threading example

This example shows how to use toast with multi threading.

```python
import threading
from deephaven import read_csv, ui


@ui.component
def csv_loader():
# The render_queue we fetch using the `use_render_queue` hook at the top of the component
render_queue = ui.use_render_queue()
table, set_table = ui.use_state()
error, set_error = ui.use_state()

def handle_submit(data):
# We define a callable that we'll queue up on our own thread
def load_table():
try:
# Read the table from the URL
t = read_csv(data["url"])

# Define our state updates in another callable. We'll need to call this on the render thread
def update_state():
set_error(None)
set_table(t)
ui.toast("Table loaded", variant="positive", timeout=5000)

# Queue up the state update on the render thread
render_queue(update_state)
except Exception as e:
# In case we have any errors, we should show the error to the user. We still need to call this from the render thread,
# so we must assign the exception to a variable and call the render_queue with a callable that will set the error
error_message = e

def update_state():
set_table(None)
set_error(error_message)
ui.toast(
f"Unable to load table: {error_message}",
variant="negative",
timeout=5000,
)

# Queue up the state update on the render thread
render_queue(update_state)

# Start our own thread loading the table
threading.Thread(target=load_table).start()

return [
# Our form displaying input from the user
ui.form(
ui.flex(
ui.text_field(
default_value="https://media.githubusercontent.com/media/deephaven/examples/main/DeNiro/csv/deniro.csv",
label="Enter URL",
label_position="side",
name="url",
flex_grow=1,
),
ui.button(f"Load Table", type="submit"),
gap=10,
),
on_submit=handle_submit,
),
(
# Display a hint if the table is not loaded yet and we don't have an error
ui.illustrated_message(
ui.heading("Enter URL above"),
ui.content("Enter a URL of a CSV above and click 'Load' to load it"),
)
if error is None and table is None
else None
),
# The loaded table. Doesn't show anything if it is not loaded yet
table,
# An error message if there is an error
(
ui.illustrated_message(
ui.icon("vsWarning"),
ui.heading("Error loading table"),
ui.content(f"{error}"),
)
if error != None
else None
),
]


my_loader = csv_loader()
```

mofojed marked this conversation as resolved.
Show resolved Hide resolved
## API Reference

```{eval-rst}
.. dhautofunction:: deephaven.ui.toast
```
4 changes: 4 additions & 0 deletions plugins/ui/docs/sidebar.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@
"label": "time_field",
"path": "components/time_field.md"
},
{
"label": "toast",
"path": "components/toast.md"
},
{
"label": "toggle_button",
"path": "components/toggle_button.md"
Expand Down
89 changes: 89 additions & 0 deletions plugins/ui/src/deephaven/ui/_internal/EventContext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from __future__ import annotations

import threading
from typing import (
Any,
Callable,
Dict,
Optional,
Generator,
)
from contextlib import contextmanager
from .NoContextException import NoContextException

OnEventCallable = Callable[[str, Dict[str, Any]], None]
"""
Callable that is called when an event is queued up.
"""

_local_data = threading.local()


def get_event_context() -> EventContext:
"""
Gets the currently active context, or throws NoContextException if none is set.

Returns:
The active EventContext, or throws if none is present.
"""
try:
return _local_data.event_context
except AttributeError:
raise NoContextException("No context set")


def _set_event_context(context: Optional[EventContext]):
"""
Set the current context for the thread. Can be set to None to unset the context for a thread.
"""
if context is None:
del _local_data.event_context
else:
_local_data.event_context = context


class EventContext:
_on_send_event: OnEventCallable
"""
The callback to call when sending an event.
"""

def __init__(
self,
on_send_event: OnEventCallable,
):
"""
Create a new event context.

Args:
on_send_event: The callback to call when sending an event.
"""

self._on_send_event = on_send_event

@contextmanager
def open(self) -> Generator[EventContext, None, None]:
"""
Opens this context.

Returns:
A context manager to manage EventContext resources.
"""
old_context: Optional[EventContext] = None
try:
old_context = get_event_context()
except NoContextException:
pass
_set_event_context(self)
yield self
_set_event_context(old_context)

def send_event(self, name: str, params: Dict[str, Any]) -> None:
"""
Send an event to the client.

Args:
name: The name of the event.
params: The params of the event.
"""
self._on_send_event(name, params)
2 changes: 2 additions & 0 deletions plugins/ui/src/deephaven/ui/_internal/NoContextException.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class NoContextException(Exception):
pass
5 changes: 1 addition & 4 deletions plugins/ui/src/deephaven/ui/_internal/RenderContext.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from deephaven.liveness_scope import LivenessScope
from contextlib import contextmanager
from dataclasses import dataclass
from .NoContextException import NoContextException

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -127,10 +128,6 @@ def _should_retain_value(value: ValueWithLiveness[T | None]) -> bool:
_local_data = threading.local()


class NoContextException(Exception):
pass


def get_context() -> RenderContext:
"""
Gets the currently active context, or throws NoContextException if none is set.
Expand Down
5 changes: 5 additions & 0 deletions plugins/ui/src/deephaven/ui/_internal/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from .EventContext import (
EventContext,
OnEventCallable,
get_event_context,
)
from .RenderContext import (
RenderContext,
StateKey,
Expand Down
2 changes: 2 additions & 0 deletions plugins/ui/src/deephaven/ui/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
from .text_area import text_area
from .text_field import text_field
from .time_field import time_field
from .toast import toast
from .toggle_button import toggle_button
from .view import view

Expand Down Expand Up @@ -132,6 +133,7 @@
"text_area",
"text_field",
"time_field",
"toast",
"toggle_button",
"view",
]
Loading
Loading