-
Notifications
You must be signed in to change notification settings - Fork 17
feat: DH-21898: Local routing #1348
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
Open
jnumainville
wants to merge
32
commits into
deephaven:main
Choose a base branch
from
jnumainville:21898_local_routing_impl
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 9 commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
e661ba6
initial impl
jnumainville e7c0cd1
fix
jnumainville 9d3b86b
warning
jnumainville 4cc8e3d
fixt comments and failures
jnumainville 3c3ae1a
docs
jnumainville d18f35a
more docs
jnumainville 1e79e03
even more docs
jnumainville 787a36b
snapshots
jnumainville 06255a2
fixes
jnumainville 1e478a3
attempt escape
jnumainville 4989268
missed a spot
jnumainville 71e6847
maybe this?
jnumainville eb9ad57
testing
jnumainville 660d988
block
jnumainville c8dd2e8
another try
jnumainville c65db2f
love when docs don't save
jnumainville 7fc038c
anotha one
jnumainville 6f14ca7
and now for something completely different
jnumainville f30c734
?????
jnumainville 2a81fd3
try strings
jnumainville 94f5ff4
these do not want to be fixed
jnumainville 95a1733
oops
jnumainville bc6b59c
maybe need that too
jnumainville a45ae84
wip
jnumainville 47cd622
remove
jnumainville b7bc5c1
missing tests
jnumainville f5556a6
fix test
jnumainville 277b686
a few improvements
jnumainville e171932
test fixes
jnumainville e2e0b22
rework
jnumainville 4055480
fix tests
jnumainville 5614407
Merge remote-tracking branch 'origin/main' into 21898_local_routing_impl
jnumainville File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| --- | ||
| name: writing-docs | ||
| description: Write documentation. Use when asked to write documentation, update docs, or build plugin docs. | ||
| --- | ||
|
|
||
| ## Documenting Functions | ||
|
|
||
| For plugins that support it (indicated by the presence of a `make_docs.py` file, e.g. `plugins/ui/make_docs.py` or `plugins/plotly-express/make_docs.py`), document functions using the `dhautofunction` directive rather than building any table or description manually. | ||
|
|
||
| The functions themselves should be fully documented with docstrings in the source code. Parameters with multiple lines in their description should use an indented block after the first line. | ||
|
|
||
| ### Example | ||
|
|
||
| ````markdown | ||
| ## API Reference | ||
|
|
||
| ```{eval-rst} | ||
| .. dhautofunction:: deephaven.ui.use_navigate | ||
| ``` | ||
| ```` | ||
|
|
||
| Use the fully qualified Python path to the function as the argument to `dhautofunction`. This automatically generates the function signature, parameters, return type, and description from the source docstring. | ||
|
|
||
| ## Document Structure | ||
|
|
||
| Follow this consistent structure when writing docs for components or hooks: | ||
|
|
||
| 1. **H1 title** — short name of the component or hook | ||
| 2. **Brief description** — one or two sentences explaining what it does and when to use it | ||
| 3. **`## Example`** — a minimal, runnable code example | ||
| 4. **Screenshot** (components only) — `` | ||
| 5. **`## UI recommendations`** (components) or **`## Recommendations`** (hooks) — numbered list of best practices and usage guidance | ||
| 6. **Additional sections** — more examples showing advanced usage, data sources, variants, etc. | ||
| 7. **`## API Reference`** — always last, using `dhautofunction` | ||
|
|
||
| For plotly-express chart docs, use `## What are X useful for?` with bullet points in place of a recommendations section. | ||
|
|
||
| ## File Placement | ||
|
|
||
| - Component docs: `plugins/ui/docs/components/<component_name>.md` | ||
| - Hook docs: `plugins/ui/docs/hooks/<hook_name>.md` | ||
| - Plot docs: `plugins/plotly-express/docs/<chart_name>.md` | ||
|
|
||
| ## Code Block Annotations | ||
|
|
||
| Docs use MyST Markdown (`.md` files with embedded RST directives). Python code blocks support two special annotations: | ||
|
|
||
| - `order=var1,var2,...` — controls which variables are shown in Deephaven and in what order. Variables prefixed with `_` are hidden (useful for intermediate tables or setup code that shouldn't be displayed). | ||
| - `skip-test` — excludes the code block from automated testing (use sparingly, e.g. for pseudocode or illustrative snippets). | ||
|
|
||
| Example: | ||
|
|
||
| ````markdown | ||
| ```python order=line_plot,my_table | ||
| import deephaven.plot.express as dx | ||
| my_table = dx.data.stocks() | ||
| line_plot = dx.line(my_table, x="Timestamp", y="Price") | ||
| ``` | ||
| ```` | ||
|
|
||
| ## Cross-References | ||
|
|
||
| Link to other docs using relative markdown paths: | ||
|
|
||
| ```markdown | ||
| Consider using [`action_button`](./action_button.md) for task-based actions. | ||
| ``` | ||
|
|
||
| ## Screenshots | ||
|
|
||
| Component screenshots are stored in `plugins/ui/docs/_assets/` and named descriptively (e.g. `button_basic.png`). Reference them with a relative path from the component doc: | ||
|
|
||
| ```markdown | ||
|  | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| # Router | ||
|
|
||
| `ui.router` is a component that matches the current URL path against provided routes and renders the matching route's element. Use it with [`route`](#route) to define hierarchical navigation structures. | ||
|
|
||
| > [!NOTE] | ||
| > Deephaven and all custom components share the path. Avoid using routers, the path, path parameters, and navigation in shared components to prevent conflicts. Do not use the route segment `/-/` in your application path as it is reserved for internal use by Deephaven. | ||
|
|
||
| ## Example | ||
|
|
||
| ```python order=app | ||
| from deephaven import ui | ||
|
|
||
|
|
||
| @ui.component | ||
| def home_page(): | ||
| return ui.text("Home page") | ||
|
|
||
| @ui.component | ||
| def app(): | ||
| # Index routes match the parent path exactly, so this renders home_page at the root URL | ||
| return ui.router( | ||
| ui.route(index=True, element=home_page), | ||
| ) | ||
|
|
||
|
|
||
| app = app() | ||
| ``` | ||
|
|
||
| ## Router Options | ||
|
|
||
| Build a simple app with a router, nested routes, route parameters, and a fallback "not found" page. | ||
|
|
||
| ```python order=app | ||
| from deephaven import ui | ||
|
|
||
|
|
||
| @ui.component | ||
| def nav_links(): | ||
| # Reuse navigation across pages for convenience | ||
| navigate = ui.use_navigate() | ||
| return ui.button_group( | ||
| ui.action_button("Home", on_press=lambda: navigate("/")), | ||
| ui.action_button("All Users", on_press=lambda: navigate("/users")), | ||
| ui.action_button("User 1", on_press=lambda: navigate("/users/1")), | ||
| ui.action_button("User 2", on_press=lambda: navigate("/users/2")), | ||
| ) | ||
|
|
||
|
|
||
| @ui.component | ||
| def user_page(): | ||
| # The use_params hook gives access to route parameters defined in the path | ||
| params = ui.use_params() | ||
| # user_id is optional due to the ? in the route path, so provide a default value | ||
| user_id = params.get("user_id", None) | ||
| if user_id is None: | ||
| return ui.flex( | ||
| nav_links(), | ||
| ui.text("All users page"), | ||
| direction="column", | ||
| ) | ||
| return ui.flex( | ||
| nav_links(), | ||
| ui.text(f"User profile for user {user_id}"), | ||
| direction="column", | ||
|
jnumainville marked this conversation as resolved.
|
||
| ) | ||
|
|
||
|
|
||
| @ui.component | ||
| def dashboard(): | ||
| return ui.flex( | ||
| nav_links(), | ||
| ui.text("Dashboard home"), | ||
| direction="column", | ||
| ) | ||
|
|
||
|
|
||
| @ui.component | ||
| def not_found(): | ||
| return ui.flex( | ||
| nav_links(), | ||
| ui.text("Page not found"), | ||
| direction="column", | ||
| ) | ||
|
|
||
|
|
||
| @ui.component | ||
| def app(): | ||
| return ui.router( | ||
| # Nest routes for hierarchical paths | ||
| ui.route( | ||
| # Match /users/{user_id} and extract user_id as an optional param | ||
| ui.route( | ||
| path="{user_id?}", | ||
| element=user_page, | ||
| ), | ||
| path="users", | ||
| ), | ||
| # An index route matches the path exactly, so this matches the root path / | ||
| ui.route(index=True, element=dashboard), | ||
| # Match any unmatched path with a wildcard route | ||
| ui.route(path="*", element=not_found), | ||
| ) | ||
|
|
||
|
|
||
| app = app() | ||
| ``` | ||
|
|
||
| This produces the following route table: | ||
|
|
||
| | URL Path | Matched Element | Params | | ||
| | ---------------- | --------------- | ------------------------ | | ||
| | `/` | `dashboard` | `{}` | | ||
| | `/users` | `user_page` | `{}` | | ||
|
jnumainville marked this conversation as resolved.
|
||
| | `/users/42` | `user_page` | `{"user_id": "42"}` | | ||
| | `/anything-else` | `not_found` | `{"*": "anything-else"}` | | ||
|
|
||
| ## Recommendations | ||
|
|
||
| 1. Include a wildcard route (`path="*"`) as a fallback so unmatched paths render a meaningful "not found" message instead of an error. | ||
| 2. Use an `index=True` route to define what renders at the exact parent path (such as a landing page at `/`). | ||
| 3. Use [`use_params`](../hooks/use_params.md) inside routed components to access route parameters, and [`use_path`](../hooks/use_path.md) for the current path. | ||
| 4. Use [`use_navigate`](../hooks/use_navigate.md) or [`link`](./link.md) with `to` to navigate between routes. | ||
|
|
||
| ## API Reference | ||
|
|
||
| ### Router | ||
|
|
||
| ```{eval-rst} | ||
| .. dhautofunction:: deephaven.ui.router | ||
| ``` | ||
|
|
||
| #### Matching behavior | ||
|
|
||
| 1. Static segments are preferred over parameterized segments. | ||
| 2. Longer matches (more segments) are preferred over shorter ones. | ||
| 3. Wildcard routes (`*`) have the lowest priority. | ||
| 4. Optional segments are matched if present but do not prevent a match if absent. | ||
| 5. Index routes match only the exact parent path. | ||
| 6. If no route matches, the router renders an error. | ||
|
|
||
| ### Route | ||
|
|
||
| ```{eval-rst} | ||
| .. dhautofunction:: deephaven.ui.route | ||
| ``` | ||
|
|
||
| #### Path patterns | ||
|
|
||
| - `{var_name}`: Required dynamic segment | ||
| - `{var_name?}`: Optional dynamic segment (matches zero or one segments) | ||
| - `*`: Wildcard, matches any remaining path segments | ||
| - Static text: Exact match | ||
|
|
||
| See [`use_params`](../hooks/use_params.md) for more details on route parameters. | ||
|
|
||
| Child paths are appended to parent paths. For example, `ui.route(ui.route(path="{user_id}"), path="users")` produces `/users/{user_id}`. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| # use_navigate | ||
|
|
||
| `use_navigate` is a hook that returns a function to trigger single page application (SPA) navigation within Deephaven. For declarative navigation, consider using [`link`](../components/link.md) with the `to` prop instead. | ||
|
|
||
| > [!NOTE] | ||
| > Deephaven and all custom components share the path. Avoid using routers, the path, path parameters, and navigation in shared components to prevent conflicts. Do not use the route segment `/-/` in your application path as it is reserved for internal use by Deephaven. | ||
|
|
||
| ## Example | ||
|
|
||
| ```python order=app | ||
| from deephaven import ui | ||
|
|
||
|
|
||
| @ui.component | ||
| def app(): | ||
| # Navigate to the settings page | ||
| navigate = ui.use_navigate() | ||
| return ui.action_button( | ||
| "Go to settings", on_press=lambda: navigate("/settings") | ||
| ) | ||
|
|
||
|
|
||
| app = app() | ||
| ``` | ||
|
|
||
| ## Navigation Options | ||
|
|
||
| Use `use_navigate` together with `use_path` and `use_query_params` to build a simple navigation system that updates the path and displays query parameters. | ||
|
|
||
| ```python order=app | ||
| from deephaven import ui | ||
|
|
||
|
|
||
| @ui.component | ||
| def navigation_demo(): | ||
| path = ui.use_path() | ||
| query_params = ui.use_query_params() | ||
| navigate = ui.use_navigate() | ||
|
|
||
| def go_dashboard(): | ||
| # Navigate to a page with a query parameter | ||
| navigate("/dashboard", query_params={"welcome": "true"}) | ||
|
jnumainville marked this conversation as resolved.
|
||
|
|
||
| def go_settings(): | ||
| # Use replace=False to push a new history entry instead of replacing the current one | ||
| navigate("/settings", replace=False) | ||
|
|
||
| def scroll_to_section(): | ||
| # Jump to a fragment on the current page | ||
| navigate(fragment="section-2") | ||
|
|
||
| def filter_by_tags(): | ||
| # Update query parameters on the current page | ||
| navigate(query_params={"tag": ["python", "java"]}) | ||
|
|
||
| return ui.flex( | ||
| ui.text(f"Current path: {path}"), | ||
| ui.text(f"Query params: {query_params}"), | ||
| ui.action_button("Dashboard", on_press=go_dashboard), | ||
| ui.action_button("Settings (push)", on_press=go_settings), | ||
| ui.action_button("Jump to section", on_press=scroll_to_section), | ||
| ui.action_button("Filter by tags", on_press=filter_by_tags), | ||
| direction="column", | ||
| ) | ||
|
|
||
|
|
||
| app = navigation_demo() | ||
| ``` | ||
|
|
||
| ## Recommendations | ||
|
|
||
| 1. Prefer [`link`](../components/link.md) with `to` for user-clickable navigation. Reserve `use_navigate` for programmatic navigation triggered by events or side effects. | ||
| 2. Use `replace=True` (the default) when navigating in response to a state change to avoid polluting the browser history. | ||
| 3. Pair with [`router`](../components/router.md) and [`route`](../components/router.md) to define the route structure that `use_navigate` targets. | ||
|
|
||
| ## API Reference | ||
|
|
||
| ```{eval-rst} | ||
| .. dhautofunction:: deephaven.ui.use_navigate | ||
| ``` | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.