Skip to content

Commit

Permalink
feat: Toast Implementation (#1030)
Browse files Browse the repository at this point in the history
closes #410
closes #874

---------

Co-authored-by: Mike Bender <mikebender@deephaven.io>
  • Loading branch information
dgodinez-dh and mofojed authored Nov 26, 2024
1 parent 5c4ed42 commit e53b322
Show file tree
Hide file tree
Showing 16 changed files with 560 additions and 20 deletions.
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!"),
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()
```

## 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

0 comments on commit e53b322

Please sign in to comment.