Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
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
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ FROM python:3.13-slim

RUN apt update && apt install -y \
curl \
g++
g++ \
libosmesa6-dev \
libx11-dev \
libxrender1

ENV POETRY_HOME=/poetry
ENV PATH=/poetry/bin:$PATH
Expand Down
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,18 @@ This repository contains examples of how to build application functionalities in

1. [Creating a form for a Pydantic model](examples/pydantic_form)
2. [Create a JSON editor for a Pydantic model](examples/pydantic_monaco)
3. [Working with Plotly](examples/plotly)
4. [Working with Matplotlib](examples/matplotlib)
5. [Synchronizing changes between tabs](examples/multitab)
3. [Conditionally rendering elements](examples/conditional_rendering)
4. [Conditionally disabling elements](examples/conditional_disabling)
5. [Changing Pydantic rules based on user input](examples/dynamic_pydantic_rules)
6. [Complex Pydantic rules](examples/complex_pydantic_rules)
7. [Using datafiles from the Analysis Cluster](examples/data_selector)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move this to a separate ORNL Only section?, ditto ONCat.

8. [Selecting datafiles from ONCat](examples/oncat)
9. [Running an NDIP tool](examples/run_ndip_tool)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change NDIP to Galaxy

10. [Running an NDIP workflow](examples/run_ndip_workflow)
11. [Working with Plotly](examples/plotly)
12. [Working with Matplotlib](examples/matplotlib)
13. [Working with VTK](examples/vtk)
14. [Synchronizing changes between tabs](examples/multitab)

## Running Examples

Expand All @@ -21,9 +30,9 @@ poetry run python examples/{example_folder}/main.py

## Example Structure

We use the MVVM framework for NOVA applications. With this in mind, each example is broken down into the following files:
We use the MVVM framework for NOVA applications. With this in mind, each example is broken down into the following sections:

1. view.py - Sets up a Trame GUI for the example.
2. model.py - Sets up a Pydantic model and any business logic needed by the application.
3. view_model.py - Sets up a view model that binds the model and view.
1. view - Sets up a Trame GUI for the example.
2. model - Sets up a Pydantic model and any business logic needed by the application.
3. view_model - Sets up a view model that binds the model and view.
4. main.py - Entrypoint for the Trame GUI.
12 changes: 12 additions & 0 deletions examples/complex_pydantic_rules/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Runs the complex Pydantic rules example."""

from examples.complex_pydantic_rules.view import App


def main() -> None:
app = App()
app.server.start(open_browser=False)


if __name__ == "__main__":
main()
51 changes: 51 additions & 0 deletions examples/complex_pydantic_rules/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
Model implementation for complex Pydantic rules example.

Here, we will create a Pydantic rule to use nltk to clean and tokenize text provided by the user.
"""

import re

import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from pydantic import BaseModel, Field, model_validator
from typing_extensions import Self


class FormData(BaseModel):
"""Pydantic model for the form data."""

tokenized_text: str = Field(default="", title="Tokenized Text")
text: str = Field(default="", title="Input Text")

# Now, we will leverage Pydantic to clean and tokenize the text.
# One could also use a Pydantic computed_field here.
@model_validator(mode="after")
def tokenize_text(self) -> Self:
input_text = self.text.lower()

# If any field in the model is invalid, then you can raise a ValueError and display the error to the user.
if "error" in input_text:
raise ValueError("Text must not contain the word error.")

# Remove punctuation and other non-word text.
cleaned_text = re.sub(r"[\W]", " ", input_text)

# Tokenize the text and remove stopwords.
words = word_tokenize(cleaned_text)
stop_words = set(stopwords.words("english"))
self.tokenized_text = " ".join([word for word in words if word not in stop_words])

# This is required by Pydantic.
return self


class Model:
"""Model implementation for complex Pydantic rules example."""

def __init__(self) -> None:
nltk.download("stopwords")
nltk.download("punkt_tab")

self.form = FormData()
41 changes: 41 additions & 0 deletions examples/complex_pydantic_rules/view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""View for complex Pydantic rules example."""

from nova.mvvm.trame_binding import TrameBinding
from nova.trame import ThemedApp
from nova.trame.view.components import InputField
from nova.trame.view.layouts import VBoxLayout
from trame.widgets import vuetify3 as vuetify

from .model import Model
from .view_model import ViewModel


class App(ThemedApp):
"""View for complex Pydantic rules example."""

def __init__(self) -> None:
super().__init__()

self.create_vm()
# If you forget to call connect, then the application will crash when you attempt to update the view.
self.view_model.form_data_bind.connect("data")
# Generally, we want to initialize the view state before creating the UI for ease of use. If initialization
# is expensive, then you can defer it. In this case, you must handle the view state potentially being
# uninitialized in the UI via v_if statements.
self.view_model.update_form_data()

self.create_ui()

def create_ui(self) -> None:
with super().create_ui() as layout:
with layout.content:
with vuetify.VCard(classes="mx-auto my-4", max_width=600):
with VBoxLayout(columns=3, gap="0.5em"):
InputField(v_model="data.text", type="textarea")
InputField(v_model="data.tokenized_text", readonly=True, type="textarea")

def create_vm(self) -> None:
binding = TrameBinding(self.state)

model = Model()
self.view_model = ViewModel(model, binding)
25 changes: 25 additions & 0 deletions examples/complex_pydantic_rules/view_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""View model implementation for complex Pydantic rules example."""

from typing import Any, Dict

from nova.mvvm.interface import BindingInterface

from .model import Model


class ViewModel:
"""View model implementation for complex Pydantic rules example."""

def __init__(self, model: Model, binding: BindingInterface) -> None:
self.model = model

# self.on_update is called any time the view updates the binding.
self.form_data_bind = binding.new_bind(self.model.form, callback_after_update=self.send_cleaned_text_to_view)

def send_cleaned_text_to_view(self, results: Dict[str, Any]) -> None:
# This is necessary to send the cleaned text to the view since it's updated programmatically.
self.form_data_bind.update_in_view(self.model.form)

def update_form_data(self) -> None:
# This will fail if you haven't called connect on the binding!
self.form_data_bind.update_in_view(self.model.form)
12 changes: 12 additions & 0 deletions examples/conditional_disabling/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Runs the conditional disabling example."""

from examples.conditional_disabling.view import App


def main() -> None:
app = App()
app.server.start(open_browser=False)


if __name__ == "__main__":
main()
19 changes: 19 additions & 0 deletions examples/conditional_disabling/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Model implementation for conditional disabling example."""

from pydantic import BaseModel, Field


class FormData(BaseModel):
"""Pydantic model for the form data."""

disable_phone_field: bool = Field(default=False, title="Disable Phone Number?")
email_address: str = Field(default="", title="Email Address")
full_name: str = Field(default="", title="Full Name")
phone_number: str = Field(default="", title="Phone Number")


class Model:
"""Model implementation for conditional disabling example."""

def __init__(self) -> None:
self.form = FormData()
49 changes: 49 additions & 0 deletions examples/conditional_disabling/view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""View for conditional disabling example."""

from nova.mvvm.trame_binding import TrameBinding
from nova.trame import ThemedApp
from nova.trame.view.components import InputField
from nova.trame.view.layouts import GridLayout, HBoxLayout
from trame.widgets import vuetify3 as vuetify

from .model import Model
from .view_model import ViewModel


class App(ThemedApp):
"""View for conditional disabling example."""

def __init__(self) -> None:
super().__init__()

self.create_vm()
# If you forget to call connect, then the application will crash when you attempt to update the view.
self.view_model.form_data_bind.connect("data")
# Generally, we want to initialize the view state before creating the UI for ease of use. If initialization
# is expensive, then you can defer it. In this case, you must handle the view state potentially being
# uninitialized in the UI via v_if statements.
self.view_model.update_form_data()

self.create_ui()

def create_ui(self) -> None:
with super().create_ui() as layout:
with layout.content:
with vuetify.VCard(classes="mx-auto my-4", max_width=600):
with GridLayout(columns=2, classes="mb-2", gap="0.5em"):
InputField(v_model="data.email_address")
InputField(v_model="data.full_name")

with HBoxLayout(gap="0.5em", valign="center"):
InputField(v_model="data.disable_phone_field", type="checkbox")

# Now, we can use disable_phone_field to conditionally disable the phone number field.
# Note that because we need to bind to a parameter that doesn't start with v_ (indicating a
# v-directive in Vue.js), we need to use the Trame tuple syntax to bind to disabled.
InputField(v_model="data.phone_number", disabled=("data.disable_phone_field",))

def create_vm(self) -> None:
binding = TrameBinding(self.state)

model = Model()
self.view_model = ViewModel(model, binding)
19 changes: 19 additions & 0 deletions examples/conditional_disabling/view_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""View model implementation for conditional disabling example."""

from nova.mvvm.interface import BindingInterface

from .model import Model


class ViewModel:
"""View model implementation for conditional disabling example."""

def __init__(self, model: Model, binding: BindingInterface) -> None:
self.model = model

# self.on_update is called any time the view updates the binding.
self.form_data_bind = binding.new_bind(self.model.form)

def update_form_data(self) -> None:
# This will fail if you haven't called connect on the binding!
self.form_data_bind.update_in_view(self.model.form)
12 changes: 12 additions & 0 deletions examples/conditional_rendering/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Runs the conditional rendering example."""

from examples.conditional_rendering.view import App


def main() -> None:
app = App()
app.server.start(open_browser=False)


if __name__ == "__main__":
main()
24 changes: 24 additions & 0 deletions examples/conditional_rendering/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Model implementation for conditional rendering example."""

from pydantic import BaseModel, Field


class FormData(BaseModel):
"""Pydantic model for the form data."""

comments: str = Field(default="", title="Comments")
email_address: str = Field(default="", title="Email Address")
full_name: str = Field(default="", title="Full Name")
phone_number: str = Field(default="", title="Phone Number")
show_phone_field: bool = Field(default=False, title="Add Phone Number?")
show_comments_field: bool = Field(default=False)


class Model:
"""Model implementation for conditional rendering example."""

def __init__(self) -> None:
self.form = FormData()

def toggle_comments(self) -> None:
self.form.show_comments_field = not self.form.show_comments_field
58 changes: 58 additions & 0 deletions examples/conditional_rendering/view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""View for conditional rendering example."""

from nova.mvvm.trame_binding import TrameBinding
from nova.trame import ThemedApp
from nova.trame.view.components import InputField
from nova.trame.view.layouts import GridLayout, HBoxLayout, VBoxLayout
from trame.widgets import html
from trame.widgets import vuetify3 as vuetify

from .model import Model
from .view_model import ViewModel


class App(ThemedApp):
"""View for conditional rendering example."""

def __init__(self) -> None:
super().__init__()

self.create_vm()
# If you forget to call connect, then the application will crash when you attempt to update the view.
self.view_model.form_data_bind.connect("data")
# Generally, we want to initialize the view state before creating the UI for ease of use. If initialization
# is expensive, then you can defer it. In this case, you must handle the view state potentially being
# uninitialized in the UI via v_if statements.
self.view_model.update_form_data()

self.create_ui()

def create_ui(self) -> None:
with super().create_ui() as layout:
with layout.content:
with vuetify.VCard(classes="mx-auto my-4", max_width=600):
with GridLayout(columns=2, classes="mb-2", gap="0.5em"):
InputField(v_model="data.email_address")
InputField(v_model="data.full_name")

with HBoxLayout(classes="mb-2", gap="0.5em", valign="center"):
InputField(v_model="data.show_phone_field", type="checkbox")

# Now, we can use show_phone_field to conditionally render the phone number field.
InputField(v_if="data.show_phone_field", v_model="data.phone_number")
# Following a v_if, we can use v_else_if and v_else.
html.Span("{{ data.full_name }}'s phone number is hidden.", v_else_if="data.full_name")
html.Span("Phone number is hidden.", v_else=True)

with VBoxLayout(gap="0.5em"):
# We can also use v_show to conditionally render a field. Note that when using v_show, you won't
# be able to use v_else_if or v_else afterwards. There is a deeper discussion of the differences
# between v_if and v_show in the Vue.js documentation: https://vuejs.org/guide/essentials/conditional#v-if-vs-v-show.
vuetify.VBtn("Toggle Comments Field", click=self.view_model.toggle_comments)
InputField(v_show="data.show_comments_field", v_model="data.comments", type="textarea")

def create_vm(self) -> None:
binding = TrameBinding(self.state)

model = Model()
self.view_model = ViewModel(model, binding)
Loading