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 2a06b71be29270a709781ecc1ab27afb466cefeb..e1d1dc7859f238e646d601c96eb762ac633494bd 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 c554a222ae7bcaedbeac384a9598c954c6d8b633..d3941e81117f72b0d2df6f76817526bd292c102c 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 know when to update the view. 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 b46f0f3b2980a78adaf74fd56a0d6f27708034d1..85bfc1f9fa369914869a7adef0af0e537e01927f 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 2000c009d154074f7355eba4f5bc53a6beb60144..7f1bd3a036fbc16c3755e0b868c492b54e2d4274 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 3dc709d745563e3783d1ec2d31d246c5f133b669..ccdfc4e2696a8d1a7f1df1642278df3ae7e46565 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,8 +1,7 @@ """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.""" @@ -11,11 +10,5 @@ class SampleTab1: 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"]) + 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 da4746cf11110b14461c0c3ea9abfe4983050f02..46765f0aa2b1c51c28a7638993f8846b6c0c5677 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") diff --git a/episodes/05-Working-with-Trame.md b/episodes/05-Working-with-Trame.md index 3dbb0f424085c700af75a03162e8bd05e4753190..fb210aacbd592583ad77b204f54a3edc7575bb92 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):