From 696f9f2eda8a4b1c935e687e324427bd1468471e Mon Sep 17 00:00:00 2001 From: John Duggan Date: Tue, 11 Feb 2025 16:20:07 -0500 Subject: [PATCH 1/3] Update section 5 --- .../src/nova_tutorial/app/models/fractal.py | 24 +-- .../src/nova_tutorial/app/view_models/main.py | 1 + .../nova_tutorial/app/views/fractal_tab.py | 15 +- .../src/nova_tutorial/app/views/main.py | 14 +- .../nova_tutorial/app/views/sample_tab_1.py | 20 +-- episodes/05-Working-with-Trame.md | 147 ++++++++---------- 6 files changed, 101 insertions(+), 120 deletions(-) diff --git a/code/episode_5/src/nova_tutorial/app/models/fractal.py b/code/episode_5/src/nova_tutorial/app/models/fractal.py index 2a06b71b..e1d1dc78 100755 --- a/code/episode_5/src/nova_tutorial/app/models/fractal.py +++ b/code/episode_5/src/nova_tutorial/app/models/fractal.py @@ -1,30 +1,30 @@ import os +from base64 import b64encode + from pydantic import BaseModel, Field from nova.galaxy import Connection, Parameters, Tool class Fractal(BaseModel): - fractal_type: str = Field(default="mandelbrot", description="Type of fractal to generate") + fractal_type_options: list[str] = ["mandelbrot", "julia", "random", "markus"] + fractal_type: str = Field(default="mandelbrot") galaxy_url: str = Field(default_factory=lambda: os.getenv("GALAXY_URL"), description="NDIP Galaxy URL") galaxy_key: str = Field(default_factory=lambda: os.getenv("GALAXY_API_KEY"), description="NDIP Galaxy API Key") - - def set_fractal_type(self, fractal_type: str): - self.fractal_type = fractal_type + image_data: str = Field(default="", description="Base64 encoded PNG") def run_fractal_tool(self): - """Runs the fractal tool with the current fractal type.""" - if not self.galaxy_url or not self.galaxy_key: - raise Exception( - "You must specify GALAXY_URL and GALAXY_API_KEY as environment variables." - ) - conn = Connection(galaxy_url=self.galaxy_url, galaxy_key=self.galaxy_key) tool = Tool(id="neutrons_fractal") params = Parameters() + params.add_input(name="option", value=self.fractal_type) with conn.connect() as galaxy_connection: data_store = galaxy_connection.create_data_store(name="fractal_store") data_store.persist() - tool.run(data_store, params) + print("Executing fractal tool. This might take a few minutes.") + output = tool.run(data_store, params) + output.get_dataset("output").download("tmp.png") - print("Fractal tool finished successfully.") \ No newline at end of file + with open("tmp.png", "rb") as image_file: + self.image_data = f"data:image/png;base64,{b64encode(image_file.read()).decode()}" + print("Fractal tool finished successfully.") diff --git a/code/episode_5/src/nova_tutorial/app/view_models/main.py b/code/episode_5/src/nova_tutorial/app/view_models/main.py index c554a222..685d01e1 100755 --- a/code/episode_5/src/nova_tutorial/app/view_models/main.py +++ b/code/episode_5/src/nova_tutorial/app/view_models/main.py @@ -43,6 +43,7 @@ class MainViewModel: fractal_tool_thread = Thread(target=self.run_fractal_in_background, daemon=True) fractal_tool_thread.start() + # We also need to know when the tool is done running so that we can create_task(self.monitor_fractal()) def run_fractal_in_background(self) -> None: diff --git a/code/episode_5/src/nova_tutorial/app/views/fractal_tab.py b/code/episode_5/src/nova_tutorial/app/views/fractal_tab.py index b46f0f3b..85bfc1f9 100755 --- a/code/episode_5/src/nova_tutorial/app/views/fractal_tab.py +++ b/code/episode_5/src/nova_tutorial/app/views/fractal_tab.py @@ -3,6 +3,7 @@ from trame.widgets import vuetify3 as vuetify from nova.trame.view.components import InputField from nova_tutorial.app.view_models.main import MainViewModel + class FractalTab: def __init__(self, view_model: MainViewModel) -> None: self.view_model = view_model @@ -10,11 +11,11 @@ class FractalTab: self.create_ui() def create_ui(self) -> None: - InputField(v_model="config.fractal.fractal_type", classes="mb-2") - vuetify.VProgressCircular(v_if="running", indeterminate=True) - vuetify.VBtn( - "Run Fractal", - v_else=True, - click=self.view_model.run_fractal, # calls the run_fractal_tool method + InputField( + v_model="config.fractal.fractal_type", + classes="mb-2", + items="config.fractal.fractal_type_options", + type="select", ) - vuetify.VImg(src=("config.fractal.image_data",), height="400", width="400") + vuetify.VProgressCircular(v_if="running", indeterminate=True) + vuetify.VImg(v_else=True, src=("config.fractal.image_data",), height="400", width="400") diff --git a/code/episode_5/src/nova_tutorial/app/views/main.py b/code/episode_5/src/nova_tutorial/app/views/main.py index 2000c009..7f1bd3a0 100755 --- a/code/episode_5/src/nova_tutorial/app/views/main.py +++ b/code/episode_5/src/nova_tutorial/app/views/main.py @@ -4,14 +4,15 @@ import logging from nova.mvvm.trame_binding import TrameBinding from nova.trame import ThemedApp +from nova.trame.view import layouts from trame.app import get_server +from trame.widgets import vuetify3 as vuetify from ..mvvm_factory import create_viewmodels from ..view_models.main import MainViewModel from .tab_content_panel import TabContentPanel from .tabs_panel import TabsPanel - logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) @@ -23,16 +24,15 @@ class MainApp(ThemedApp): super().__init__() self.server = get_server(None, client_type="vue3") binding = TrameBinding(self.server.state) - self.server.state.trame__title = "Nova Tutorial" self.view_models = create_viewmodels(binding) self.view_model: MainViewModel = self.view_models["main"] self.create_ui() def create_ui(self) -> None: - self.state.trame__title = "Nova Tutorial" + self.state.trame__title = "Fractal Tool GUI" with super().create_ui() as layout: - layout.toolbar_title.set_text("Nova Tutorial") + layout.toolbar_title.set_text("Fractal Tool GUI") with layout.pre_content: TabsPanel(self.view_models["main"]) with layout.content: @@ -41,5 +41,9 @@ class MainApp(ThemedApp): self.view_models["main"], ) with layout.post_content: - pass + with layouts.HBoxLayout(classes="my-2", halign="center"): + vuetify.VBtn( + "Run Fractal", + click=self.view_model.run_fractal, # calls the run_fractal_tool method + ) return layout diff --git a/code/episode_5/src/nova_tutorial/app/views/sample_tab_1.py b/code/episode_5/src/nova_tutorial/app/views/sample_tab_1.py index 3dc709d7..46765f0a 100755 --- a/code/episode_5/src/nova_tutorial/app/views/sample_tab_1.py +++ b/code/episode_5/src/nova_tutorial/app/views/sample_tab_1.py @@ -1,21 +1,13 @@ -"""Module for the Sample Tab 1.""" +"""Module for the Sample Tab 2.""" -from nova.trame.view.components import InputField, RemoteFileInput -from nova.trame.view import layouts -from trame.widgets import vuetify3 as vuetify +from nova.trame.view.components import InputField -class SampleTab1: - """Sample tab 1 view class. Renders text input for username.""" + +class SampleTab2: + """Sample tab 2 view class. Renders text input for user password.""" def __init__(self) -> None: self.create_ui() def create_ui(self) -> None: - with layouts.VBoxLayout(classes="ma-2"): # Overall vertical layout - InputField(v_model="config.username", label="Username") - with layouts.HBoxLayout(): # Horizontal layout for first and last name - InputField(v_model="config.firstName", label="First Name") - InputField(v_model="config.lastName", label="Last Name") - vuetify.VCheckbox(label="Remember me") - vuetify.VSwitch(label="Enable Notifications") - RemoteFileInput(v_model="config.file", base_paths=["/SNS"]) + InputField(v_model="config.password") diff --git a/episodes/05-Working-with-Trame.md b/episodes/05-Working-with-Trame.md index 3dbb0f42..fb210aac 100755 --- a/episodes/05-Working-with-Trame.md +++ b/episodes/05-Working-with-Trame.md @@ -73,7 +73,7 @@ Let\'s explore these components in more detail: Layouts are responsible for arraging your content in a consistent manner. In Trame, a layout consists of multiple "slots". A slot is a section of the page to which you can add content. -`nova-trame` provides a basic layout and theme that you can access via the `ThemedApp` class. The template app will setup your main view class to inherit from `ThemedApp` already, so let\'s look at how the layout is defined and how we can add content to slots. +`nova-trame` provides a basic layout and theme that you can access via the `ThemedApp` class. The template app will setup your main view class to inherit from `ThemedApp` already, but to see how it works let\'s try moving the button to run the fractal tool from the fractal tab into `post_content` slot in the layout. **1. `src/nova_tutorial/app/views/main.py` (Modify):** @@ -85,16 +85,15 @@ class MainApp(ThemedApp): super().__init__() self.server = get_server(None, client_type="vue3") binding = TrameBinding(self.server.state) - self.server.state.trame__title = "Nova Tutorial" self.view_models = create_viewmodels(binding) self.view_model: MainViewModel = self.view_models["main"] self.create_ui() def create_ui(self) -> None: - self.state.trame__title = "Nova Tutorial" + self.state.trame__title = "Fractal Tool GUI" with super().create_ui() as layout: - layout.toolbar_title.set_text("Nova Tutorial") + layout.toolbar_title.set_text("Fractal Tool GUI") with layout.pre_content: TabsPanel(self.view_models["main"]) with layout.content: @@ -103,10 +102,21 @@ class MainApp(ThemedApp): self.view_models["main"], ) with layout.post_content: - pass + vuetify.VBtn( + "Run Fractal", + click=self.view_model.run_fractal # calls the run_fractal_tool method + ) return layout ``` +**2. `src/nova_tutorial/app/views/fractal_tab.py` (Modify):** + +```python + def create_ui(self) -> None: + InputField(v_model="config.fractal.fractal_type") + vuetify.VImg(src=("config.fractal.image_data",), height="400", width="400") +``` + :::::::::::::::::::::::::: callout `ThemedApp.create_ui` will return the layout object, so be careful not to modify the `super().create_ui()` call. @@ -141,10 +151,43 @@ This integration significantly reduces the amount of boilerplate code you need t The `InputField` also provides debouncing and throttling features that can improve application performance. These features are useful when dealing with user input that triggers frequent updates to the Trame state. +Let\'s change the fractal type field to a dropdown and add a label to it. + +**3. `src/nova_tutorial/app/models/fractal.py` (Modify):** + +```python +class Fractal(BaseModel): + fractal_type_options: list[str] = ["mandelbrot", "julia", "random", "markus"] + fractal_type: str = Field(default="mandelbrot") +``` + +**4. `src/nova_tutorial/app/views/fractal_tab.py` (Modify):** + +```python + InputField(v_model="config.fractal.fractal_type", items="config.fractal.fractal_type_options", type="select") +``` + ### `RemoteFileInput` The `RemoteFileInput` component allows you to quickly create a widget for the user to find and select files from the computer running your application. This can be powerful if you want to connect your application to the SNS analysis cluster filesystem, for example, as you could use `RemoteFileInput(base_paths=["/HFIR", "/SNS"])` to expose relevant experiment data to users. +**5. `src/nova_tutorial/app/views/sample_tab_1.py` (Modify):** + +```python +from nova.trame.view.components import InputField, RemoteFileInput + + +class SampleTab1: + """Sample tab 1 view class. Renders text input for username.""" + + def __init__(self) -> None: + self.create_ui() + + def create_ui(self) -> None: + RemoteFileInput(v_model="file", base_paths=["/HFIR", "/SNS"]) + InputField(v_model="config.username") +``` + :::::::::::::::::::::::::::::::: callout If you want to connect your application to the analysis cluster, then it will need to be run on a computer where the filesystem is mounted. If your application is deployed through our platform, then we can ensure that your application runs in the correct environment to support your needs. @@ -176,7 +219,7 @@ with layouts.GridLayout(columns=2): vuetify.VTextField(label="Phone Number") ``` - This code creates a grid with two columns and arranges the text fields in the grid. +This code creates a grid with two columns and arranges the text fields in the grid. * **`VBoxLayout`:** Creates a vertical box layout, stacking its children vertically. This is useful for creating simple vertical layouts. @@ -190,7 +233,7 @@ with layouts.VBoxLayout(): vuetify.VTextField(label="City") ``` - This code creates a vertical layout and stacks the text fields vertically. +This code creates a vertical layout and stacks the text fields vertically. * **`HBoxLayout`:** Creates a horizontal box layout, stacking its children horizontally. This is useful for creating simple horizontal layouts. @@ -203,83 +246,28 @@ with layouts.HBoxLayout(): vuetify.VTextField(label="Last Name") ``` - This code creates a horizontal layout and stacks the text fields horizontally. +This code creates a horizontal layout and stacks the text fields horizontally. By combining these layout components, you can create complex and responsive UI layouts. -For a more detailed explanation of how to work with our layout and theme, please refer to the [`nova-trame documentation`](https://nova-application-development.readthedocs.io/projects/nova-trame/en/stable/working_with_trame.html). - -### - -## Adding More UI Components to the Sample Tabs +As an example, we can use the layout classes to center the "Run Fractal" button. -Now, let\'s add some UI components to the Sample Tabs in our application to demonstrate how to use these components. We\'ll modify the `sample_tab_1.py` and `sample_tab_2.py` files to include these components. - -**2. `src/nova_tutorial/app/views/sample_tab_1.py` (Modify):** - -We\'ll add an `InputField` and a `VBoxLayout` to this tab. +**6. `src/nova_tutorial/app/views/main.py` (Modify):** ```python -"""Module for the Sample Tab 1.""" - -from nova.trame.view.components import InputField, RemoteFileInput from nova.trame.view import layouts -from trame.widgets import vuetify3 as vuetify -class SampleTab1: - """Sample tab 1 view class. Renders text input for username.""" +... - def __init__(self) -> None: - self.create_ui() - - def create_ui(self) -> None: - with layouts.VBoxLayout(classes="ma-2"): - InputField(v_model="config.username", label="Username") - vuetify.VCheckbox(label="Remember me") - RemoteFileInput(v_model="config.file", base_paths=["/HFIR", "/SNS"]) -``` - -Since `config.file` doesn't exist yet, we\'ll need to add it to the model. - -**3. `src/nova_tutorial/app/models/main_model.py` (Modify):** - -```python - username: str = Field( - default="test_name", - min_length=1, - title="User Name", - description="Please provide the name of the user", - examples=["user"], - ) - password: str = Field(default="test_password", title="User Password") - file: str = Field(default="", title="Select a File") - fractal: Fractal = Field(default_factory=Fractal) -``` - -**4. `src/nova_tutorial/app/views/sample_tab_2.py` (Modify):** - -We\'ll add a `GridLayout` and an `InputField` to this tab. - -```python -"""Module for the Sample Tab 2.""" - -from nova.trame.view.components import InputField -from nova.trame.view import layouts -from trame.widgets import vuetify3 as vuetify - -class SampleTab2: - """Sample tab 2 view class. Renders text input for user password.""" - - def __init__(self) -> None: - self.create_ui() - - def create_ui(self) -> None: - with layouts.GridLayout(columns=2, classes="ma-2"): - InputField(v_model="config.password", label="Password", type="password") - vuetify.VSlider(label="Volume") + with layout.post_content: + with layouts.HBoxLayout(classes="my-2", halign="center"): + vuetify.VBtn( + "Run Fractal", + click=self.view_model.run_fractal # calls the run_fractal_tool method + ) ``` -In `SampleTab1`, we\'ve used a `VBoxLayout` to vertically stack the `InputField` and `VCheckbox` components. In `SampleTab2`, we\'ve used a `GridLayout` to arrange the `InputField` and `VSlider` components in a two-column grid. +For a more detailed explanation of how to work with our layout and theme, please refer to the [`nova-trame documentation`](https://nova-application-development.readthedocs.io/projects/nova-trame/en/stable/working_with_trame.html). ## Running the application @@ -295,7 +283,7 @@ You should now see the simple UI. When you click the "Sample Tab 1" and "Sample Now that we understand the basics of working with Trame, let\'s make the view for the fractal tab a bit more intuitive for the user by giving them a visual indicator that the job is running. -**5. `src/nova_tutorial/app/views/fractal_tab.py` (Modify):** +**7. `src/nova_tutorial/app/views/fractal_tab.py` (Modify):** ```python def __init__(self, view_model: MainViewModel) -> None: @@ -304,19 +292,14 @@ Now that we understand the basics of working with Trame, let\'s make the view fo self.create_ui() def create_ui(self) -> None: - InputField(v_model="config.fractal.fractal_type", classes="mb-2") + InputField(v_model="config.fractal.fractal_type", classes="mb-2", items="config.fractal.fractal_type_options", type="select") vuetify.VProgressCircular(v_if="running", indeterminate=True) - vuetify.VBtn( - "Run Fractal", - v_else=True, - click=self.view_model.run_fractal # calls the run_fractal_tool method - ) - vuetify.VImg(src=("config.fractal.image_data",), height="400", width="400") + vuetify.VImg(v_else=True, src=("config.fractal.image_data",), height="400", width="400") ``` We will need to add a data binding for `running`, as well. We choose to place this directly in the view model as this is not relevant to running the fractal tool on NDIP. -**6. `src/nova_tutorial/app/view_models/main.py` (Modify):** +**8. `src/nova_tutorial/app/view_models/main.py` (Modify):** ```python def __init__(self, model: MainModel, binding: BindingInterface): -- GitLab From 509a15e5602d0cb6796118ddab97b254a8dfa834 Mon Sep 17 00:00:00 2001 From: John Duggan Date: Tue, 11 Feb 2025 16:21:56 -0500 Subject: [PATCH 2/3] Fix comment on create_task --- code/episode_5/src/nova_tutorial/app/view_models/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/episode_5/src/nova_tutorial/app/view_models/main.py b/code/episode_5/src/nova_tutorial/app/view_models/main.py index 685d01e1..d3941e81 100755 --- a/code/episode_5/src/nova_tutorial/app/view_models/main.py +++ b/code/episode_5/src/nova_tutorial/app/view_models/main.py @@ -43,7 +43,7 @@ class MainViewModel: fractal_tool_thread = Thread(target=self.run_fractal_in_background, daemon=True) fractal_tool_thread.start() - # We also need to know when the tool is done running so that we can + # We also need to know when the tool is done running so that we can know when to update the view. create_task(self.monitor_fractal()) def run_fractal_in_background(self) -> None: -- GitLab From fb3cea99d79a15d483b6e38902136a9d1b407d4d Mon Sep 17 00:00:00 2001 From: John Duggan Date: Tue, 11 Feb 2025 16:23:26 -0500 Subject: [PATCH 3/3] Fix sample tabs --- .../src/nova_tutorial/app/views/sample_tab_1.py | 11 ++++++----- .../src/nova_tutorial/app/views/sample_tab_2.py | 14 +------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/code/episode_5/src/nova_tutorial/app/views/sample_tab_1.py b/code/episode_5/src/nova_tutorial/app/views/sample_tab_1.py index 46765f0a..ccdfc4e2 100755 --- a/code/episode_5/src/nova_tutorial/app/views/sample_tab_1.py +++ b/code/episode_5/src/nova_tutorial/app/views/sample_tab_1.py @@ -1,13 +1,14 @@ -"""Module for the Sample Tab 2.""" +"""Module for the Sample Tab 1.""" -from nova.trame.view.components import InputField +from nova.trame.view.components import InputField, RemoteFileInput -class SampleTab2: - """Sample tab 2 view class. Renders text input for user password.""" +class SampleTab1: + """Sample tab 1 view class. Renders text input for username.""" def __init__(self) -> None: self.create_ui() def create_ui(self) -> None: - InputField(v_model="config.password") + RemoteFileInput(v_model="file", base_paths=["/HFIR", "/SNS"]) + InputField(v_model="config.username") diff --git a/code/episode_5/src/nova_tutorial/app/views/sample_tab_2.py b/code/episode_5/src/nova_tutorial/app/views/sample_tab_2.py index da4746cf..46765f0a 100755 --- a/code/episode_5/src/nova_tutorial/app/views/sample_tab_2.py +++ b/code/episode_5/src/nova_tutorial/app/views/sample_tab_2.py @@ -1,8 +1,6 @@ """Module for the Sample Tab 2.""" from nova.trame.view.components import InputField -from nova.trame.view import layouts -from trame.widgets import vuetify3 as vuetify class SampleTab2: @@ -12,14 +10,4 @@ class SampleTab2: self.create_ui() def create_ui(self) -> None: - with layouts.VBoxLayout(classes="ma-2"): # Parent vertical layout - with layouts.HBoxLayout(): # Horizontal layout for email and phone - InputField(v_model="config.email", label="Email", type="email") - InputField(v_model="config.phoneNumber", label="Phone Number", type="tel") - with layouts.GridLayout(columns=2): # Two column grid layout for remaining fields - vuetify.VSlider(label="Volume") - with layouts.VBoxLayout(classes="ma-2"): # Overall vertical layout - vuetify.VLabel("Item 1", classes="bg-primary h-100 w-100 justify-center") - vuetify.VLabel("Item 2", classes="bg-secondary h-100 w-100 justify-center") - InputField(v_model="config.address", label="Address") - InputField(v_model="config.comments", label="Comments") + InputField(v_model="config.password") -- GitLab