You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2020/06/22 15:03:49 UTC
[airavata-django-portal] 02/02: AIRAVATA-3285 Interactive parameter
documentation
This is an automated email from the ASF dual-hosted git repository.
machristie pushed a commit to branch AIRAVATA-3285--Interactive-output-view-providers
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git
commit c8fe65b81eb20ff49ed6a6b85db0f35420fadc1b
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Mon Jun 22 11:02:24 2020 -0400
AIRAVATA-3285 Interactive parameter documentation
---
docs/dev/custom_output_view_provider.md | 317 +++++++++++++++++++++
docs/tutorial/gateways2019_tutorial.md | 114 ++++++--
.../gateways19/gaussian-eigenvalues-show_grid.png | Bin 0 -> 149262 bytes
mkdocs.yml | 7 +-
4 files changed, 406 insertions(+), 32 deletions(-)
diff --git a/docs/dev/custom_output_view_provider.md b/docs/dev/custom_output_view_provider.md
new file mode 100644
index 0000000..a34f60a
--- /dev/null
+++ b/docs/dev/custom_output_view_provider.md
@@ -0,0 +1,317 @@
+# Custom Output View Provider
+
+A custom _output view provider_ generates visualizations of experiment outputs.
+Output view providers are implemented as a Python function and packaged as a
+Python package, with the requisite metadata (more on this below). An output view
+provider is associated with the output of an application in the Application
+Catalog.
+
+There are several different output view display types, such as: image, link, or
+html. If configured an output view will be displayed for an output file and the
+Airavata Django Portal will invoke the custom output view provider to get the
+data to display. For example, if the output view display type is image, then the
+output view provider will be invoked and it should return image data.
+
+## Getting started
+
+See the
+[Gateways 2019 tutorial](../tutorial/gateways2019_tutorial.md#tutorial-exercise-create-a-custom-output-viewer-for-an-output-file)
+for help on setting up a development environment and implementing a simple
+output view provider.
+
+You can use this as a starting point to create your own custom output view
+provider. Here is what you would need to change:
+
+1. First add your custom output view provider implementation to
+ `output_views.py`.
+2. Rename the Python package name in `setup.py`.
+3. Update the `install_requires` list of dependencies based on what your custom
+ output view provider requires.
+4. Rename the Python module folder from `./gateways2019_tutorial` to whatever
+ you want to call it.
+5. Rename the output view provider in the `entry_points` metadata in `setup.py`.
+ For example, if you wanted to name your output view provider
+ `earthquake-sites-visualization` and you renamed your Python module folder
+ from `./gateways2019_tutorial` to `./earthquake_gateway`, then you could have
+ the following in the `entry_points`:
+
+```
+...
+ entry_points="""
+[airavata.output_view_providers]
+earthquake-sites-visualization = earthquake_gateway.output_views:EarthquakeSitesViewProvider
+""",
+```
+
+6. If you don't need a Django app, you can remove the `[airavata.djangoapp]`
+ section from `entry_points`.
+
+Please note, if you update `setup.py` and you're doing local development, you'll
+need to reinstall the package into your local Django instance's virtual
+environment using:
+
+```bash
+python setup.py develop
+```
+
+## Reference
+
+### Output View Provider interface
+
+Output view providers should be defined as a Python class. They should define
+the following attributes:
+
+- `display_type`: this should be one of _link_, _image_ or _html_.
+- `name`: this is the name of the output view provider displayed to the user.
+- `test_output_file`: (optional) the path to a file to use for testing
+ purposes. This file will be passed to the `generate_data` function as the
+ `output_file` parameter when the output file isn't available and the Django
+ server is running in DEBUG mode. This is helpful when developing a custom
+ output view provider in a local Django instance that doesn't have access to
+ the output files.
+
+The output view provider class should define the following method:
+
+```python
+def generate_data(self, request, experiment_output, experiment, output_file=None, **kwargs):
+
+ # Return a dictionary
+ return {
+ #...
+ }
+```
+
+The required contents of the dictionary varies based on the _display type_.
+
+#### Display type link
+
+The returned dictionary should include the following entries:
+
+- url
+- label
+
+The _label_ is the text of the link. Generally speaking this will be rendered
+as:
+
+```html
+<a href="{{ url }}">{{ label }}</a>
+```
+
+**Examples**
+
+- [SimCCS Maptool - SolutionLinkProvider](https://github.com/SciGaP/simccs-maptool/blob/master/simccs_maptool/output_views.py#L5)
+
+#### Display type image
+
+The returned dictionary should include the following entries:
+
+- image: a stream of bytes, i.e., either the result of `open(file, 'rb')` or
+ something equivalent like `io.BytesIO`.
+- mime-type: the mime-type of the image, for example, `image/png`.
+
+**Examples**
+
+- [AMP Gateway - TRexXPlotViewProvider](https://github.com/SciGaP/amp-gateway-django-app/blob/master/amp_gateway/plot.py#L115)
+
+#### Display type html
+
+The returned dictionary should include the following entries:
+
+- output: a raw HTML string
+- js: a static URL to a JavaScript file, for example,
+ `/static/earthquake_gateway/custom-leaflet-script.js`.
+
+**Examples**
+
+- [dREG - DregGenomeBrowserViewProvider](https://github.com/SciGaP/dreg-djangoapp/blob/master/dreg_djangoapp/output_views.py#L4)
+
+### Entry Point registration
+
+Custom output view providers are packaged as Python packages in order to be
+deployed into an instance of the Airavata Django Portal. The Python package must
+have metadata that indicates that it contains a custom output view provider.
+This metadata is specified as an _entry point_ in the package's `setup.py` file
+under the named parameter `entry_points`.
+
+The entry point must be added to an entry point group called
+`[airavata.output_view_providers]`. The entry point format is:
+
+```
+label = module:class
+```
+
+The _label_ is the identifier you will use when associating an output view
+provider with an output file in the Application Catalog. As such, you can name
+it whatever you want. The _module_ must be the Python module in which exists
+your output view provider. The _class_ must be the name of your output view
+provider class.
+
+See the **Getting Started** section for an example of how to format the entry
+point in `setup.py`.
+
+### Associating an output view provider with an output file
+
+In the Application Catalog, you can add JSON metadata to associate an output
+view provider with an output file.
+
+1. In the top navigation menu in the Airavata Django Portal, go to **Settings**.
+2. If not already selected, select the **Application Catalog** from the left
+ hand side navigation.
+3. Click on the application.
+4. Click on the **Interface** tab.
+5. Scroll down to the _Output Fields_ and find the output file with which you
+ want to associate the output view provider.
+6. In the _Metadata_ field, add or update the `output-view-providers` key. The
+ value should be an array (beginning and ending with square brackets). The
+ name of your output view provider is the label you gave it when you created
+ the entry point.
+
+The _Metadata_ field will have a value like this:
+
+```json
+{
+ "output-view-providers": ["gaussian-eigenvalues-plot"]
+}
+```
+
+Where instead of `gaussian-eigenvalues-plot` you would put or add the label of
+your custom output view provider.
+
+There's a special `default` output view provider that provides the default
+interface for output files, namely by providing a download link for the output
+file. This `default` output view provider will be shown initially to the user
+and the user can then select a custom output view provider from a drop down
+menu. If, instead, you would like your custom output view provider to be
+displayed initially, you can add the `default` view provider in the list of
+output-view-providers and place it second. For example:
+
+```json
+{
+ "output-view-providers": ["gaussian-eigenvalues-plot", "default"]
+}
+```
+
+would make the `gaussian-eigenvalues-plot` the initial output view provider. The
+user can access the default output view provider from the drop down menu.
+
+### Interactive parameters
+
+You can add some interactivity to your custom output view provider by adding one
+or more interactive parameters. An interactive parameter is a parameter that
+your custom output view provider declares, with a name and current value. The
+Airavata Django Portal will display all interactive parameters in a form and
+allow the user to manipulate them. When an interactive parameter is updated by
+the user, your custom output view provider will be again invoked with the new
+value of the parameter.
+
+To add an interactive parameter, you first need to add a keyword parameter to
+your `generate_data` function. For example, let's say you want to add a boolean
+`show_grid` parameter that the user can toggle on and off. You would change the
+signature of the `generate_data` function to:
+
+```python
+def generate_data(self, request, experiment_output, experiment, output_file=None, show_grid=False, **kwargs):
+
+ # Return a dictionary
+ return {
+ #...
+ }
+```
+
+In this example, the default value of `show_grid` is `False`, but you can make
+it `True` instead. The default value of the interactive parameter will be its
+value when it is initially invoked. It's recommended that you supply a default
+value but the default value can be `None` if there is no appropriate default
+value.
+
+Next, you need to declare the interactive parameter in the returned dictionary
+along with its current value in a special key called `interactive`. For example:
+
+```python
+def generate_data(self, request, experiment_output, experiment, output_file=None, show_grid=False, **kwargs):
+
+ # Return a dictionary
+ return {
+ #...
+ 'interactive': [
+ {'name': 'show_grid', 'value': show_grid}
+ ]
+ }
+```
+
+declares the interactive parameter named `show_grid` and its current value.
+
+The output view display will render a form showing the value of `show_grid` (in
+this case, since it is boolean, as a checkbox).
+
+#### Supported parameter types
+
+Besides boolean, the following additional parameter types are supported:
+
+| Type | UI Control | Additional options |
+| ------- | ----------------------- | ----------------------------------------------------------------------------------------------------------- |
+| Boolean | Checkbox | |
+| String | Text input | |
+| Integer | Stepper or Range slider | `min`, `max` and `step` - if `min` and `max` are supplied, renders as a range slider. `step` defaults to 1. |
+| Float | Stepper or Range slider | `min`, `max` and `step` - if `min` and `max` are supplied, renders as a range slider. |
+
+Further, if the interactive parameter defines an `options` list, this will
+render as a drop-down select. The `options` list can either be a list of
+strings, for example:
+
+```python
+def generate_data(self, request, experiment_output, experiment, output_file=None, color='red', **kwargs):
+
+ # Return a dictionary
+ return {
+ #...
+ 'interactive': [
+ {'name': 'color', 'value': color, 'options': ['red', 'green', 'blue']}
+ ]
+ }
+```
+
+Or, the `options` list can be a list of `(text, value)` tuples:
+
+```python
+def generate_data(self, request, experiment_output, experiment, output_file=None, color='red', **kwargs):
+
+ # Return a dictionary
+ return {
+ #...
+ 'interactive': [
+ {'name': 'color', 'value': color, 'options': [('Red', 'red'), ('Blue', 'blue'), ('Green', 'green')]}
+ ]
+ }
+```
+
+The `text` is what is displayed to the user as the value's label in the
+drop-down. The `value` is what will be passed to the output view provider when
+selected by the user.
+
+#### Additional configuration
+
+The following additional properties are supported:
+
+- **label** - by default the name of the interactive parameter is its label in
+ the interactive form. You can customize the label with the `label` property.
+- **help** - you can also display help text below the parameter in the
+ interactive form with the `help` property.
+
+For example:
+
+```python
+def generate_data(self, request, experiment_output, experiment, output_file=None, color='red', **kwargs):
+
+ # Return a dictionary
+ return {
+ #...
+ 'interactive': [
+ {'name': 'color',
+ 'value': color,
+ 'options': [('Red', 'red'), ('Blue', 'blue'), ('Green', 'green')],
+ 'label': 'Bar chart color',
+ 'help': 'Change the primary color of the bar chart.'}
+ ]
+ }
+```
diff --git a/docs/tutorial/gateways2019_tutorial.md b/docs/tutorial/gateways2019_tutorial.md
index d065031..1d7c0ca 100644
--- a/docs/tutorial/gateways2019_tutorial.md
+++ b/docs/tutorial/gateways2019_tutorial.md
@@ -607,6 +607,62 @@ It should look something like this:
![Screenshot of generated Gaussian eigenvalues plot](./screenshots/gateways19/gaussian-eigenvalues.png)
+### (Optional) Interactive parameter
+
+In additional to producing static visualizations, output view providers can
+declare interactive parameters that can be manipulated by the user. We can add a
+simple boolean interactive parameter to toggle the display of the matplotlib
+grid as an example.
+
+1. Change the `generate_data` function so that it has an additional `show_grid`
+ parameter with a default value of `False`:
+
+```python
+ def generate_data(self, request, experiment_output, experiment, output_file=None, show_grid=False):
+```
+
+2. Add the following `.show_grid()` lines to the matplotlib code:
+
+```python
+...
+ fig.suptitle("Eigenvalues")
+ ax = fig.subplots(2, 1)
+ ax[0].plot(range(1, 10), homo_eigenvalues, label='Homo')
+ ax[0].set_ylabel('eV')
+ ax[0].legend()
+ ax[0].show_grid(show_grid)
+ ax[1].plot(range(1, 10), lumo_eigenvalues, label='Lumo')
+ ax[1].set_ylabel('eV')
+ ax[1].legend()
+ ax[1].show_grid(show_grid)
+...
+```
+
+3. Change the resulting dictionary to have the special `interactive` property
+ and declare the `show_grid` parameter:
+
+```python
+...
+ # return dictionary with image data
+ return {
+ 'image': image_bytes,
+ 'mime-type': 'image/png'
+ 'interactive': [
+ {'name': 'show_grid', 'value': show_grid}
+ ]
+ }
+```
+
+This will provider the user with a checkbox for manipulating the show_grid
+parameter. Every time the user changes it, the GaussianEigenvaluesViewProvider
+will be again invoked. It should look something like the following:
+
+![Gaussian Eigenvalues View Provider with interactive parameter](./screenshots/gateways19/gaussian-eigenvalues-show_grid.png)
+
+There are several more interactive parameter types and additional options. You
+can learn more about them in the
+[custom output view provider documentation](../dev/custom_output_view_provider.md#interactive-parameters).
+
## Tutorial exercise: Create a custom Django app
In this tutorial exercise we'll create a fully custom user interface that lives
@@ -940,7 +996,7 @@ Now we'll use `AiravataAPI` to submit an Echo job.
value:
```javascript
-$("#run-button").click(e => {
+$("#run-button").click((e) => {
const greeting = $("#greeting-select").val();
});
```
@@ -952,7 +1008,7 @@ $("#run-button").click(e => {
```javascript
const loadAppInterface = services.ApplicationInterfaceService.retrieve({
- lookup: appInterfaceId
+ lookup: appInterfaceId,
});
```
@@ -967,7 +1023,7 @@ const loadAppInterface = services.ApplicationInterfaceService.retrieve({
const appDeploymentId =
"js-156-93.jetstream-cloud.org_Echo_37eb38ac-74c8-4aa4-a037-c656ab5bc6b8";
const loadQueues = services.ApplicationDeploymentService.getQueues({
- lookup: appDeploymentId
+ lookup: appDeploymentId,
});
```
@@ -988,15 +1044,15 @@ const loadWorkspacePrefs = services.WorkspacePreferencesService.get();
object then _save_ and _launch_ it. Here's the complete click handler:
```javascript
-$("#run-button").click(e => {
+$("#run-button").click((e) => {
const greeting = $("#greeting-select").val();
const loadAppInterface = services.ApplicationInterfaceService.retrieve({
- lookup: appInterfaceId
+ lookup: appInterfaceId,
});
const appDeploymentId =
"js-156-93.jetstream-cloud.org_Echo_37eb38ac-74c8-4aa4-a037-c656ab5bc6b8";
const loadQueues = services.ApplicationDeploymentService.getQueues({
- lookup: appDeploymentId
+ lookup: appDeploymentId,
});
const resourceHostId =
"js-156-93.jetstream-cloud.org_33019860-54c2-449b-96d3-988d4f5a501e";
@@ -1008,7 +1064,7 @@ $("#run-button").click(e => {
const experiment = appInterface.createExperiment();
experiment.experimentName = "Echo " + greeting;
experiment.projectId = workspacePrefs.most_recent_project_id;
- const cloudQueue = queues.find(q => q.queueName === queueName);
+ const cloudQueue = queues.find((q) => q.queueName === queueName);
experiment.userConfigurationData.groupResourceProfileId = groupResourceProfileId;
experiment.userConfigurationData.computationalResourceScheduling.resourceHostId = resourceHostId;
experiment.userConfigurationData.computationalResourceScheduling.totalCPUCount =
@@ -1023,9 +1079,9 @@ $("#run-button").click(e => {
return services.ExperimentService.create({ data: experiment });
})
- .then(exp => {
+ .then((exp) => {
return services.ExperimentService.launch({
- lookup: exp.experimentId
+ lookup: exp.experimentId,
});
});
});
@@ -1063,17 +1119,17 @@ We'll read the STDOUT file and display that in our experiment listing table.
```javascript
if (exp.experimentStatus === models.ExperimentState.COMPLETED) {
services.FullExperimentService.retrieve({ lookup: exp.experimentId }).then(
- fullDetails => {
+ (fullDetails) => {
const stdoutDataProductId = fullDetails.experiment.experimentOutputs.find(
- o => o.name === "Echo-STDOUT"
+ (o) => o.name === "Echo-STDOUT"
).value;
const stdoutDataProduct = fullDetails.outputDataProducts.find(
- dp => dp.productUri === stdoutDataProductId
+ (dp) => dp.productUri === stdoutDataProductId
);
if (stdoutDataProduct && stdoutDataProduct.downloadURL) {
return fetch(stdoutDataProduct.downloadURL, {
- credentials: "same-origin"
- }).then(result => result.text());
+ credentials: "same-origin",
+ }).then((result) => result.text());
}
}
);
@@ -1085,20 +1141,20 @@ if (exp.experimentStatus === models.ExperimentState.COMPLETED) {
```javascript
if (exp.experimentStatus === models.ExperimentState.COMPLETED) {
services.FullExperimentService.retrieve({ lookup: exp.experimentId })
- .then(fullDetails => {
+ .then((fullDetails) => {
const stdoutDataProductId = fullDetails.experiment.experimentOutputs.find(
- o => o.name === "Echo-STDOUT"
+ (o) => o.name === "Echo-STDOUT"
).value;
const stdoutDataProduct = fullDetails.outputDataProducts.find(
- dp => dp.productUri === stdoutDataProductId
+ (dp) => dp.productUri === stdoutDataProductId
);
if (stdoutDataProduct && stdoutDataProduct.downloadURL) {
return fetch(stdoutDataProduct.downloadURL, {
- credentials: "same-origin"
- }).then(result => result.text());
+ credentials: "same-origin",
+ }).then((result) => result.text());
}
})
- .then(text => {
+ .then((text) => {
$(`#output_${index}`).text(text);
});
}
@@ -1121,8 +1177,8 @@ function loadExperiments() {
limit: 5,
[models.ExperimentSearchFields.USER_NAME.name]:
session.Session.username,
- [models.ExperimentSearchFields.APPLICATION_ID.name]: appInterfaceId
- }).then(data => {
+ [models.ExperimentSearchFields.APPLICATION_ID.name]: appInterfaceId,
+ }).then((data) => {
$("#experiment-list").empty();
data.results.forEach((exp, index) => {
$("#experiment-list").append(
@@ -1137,28 +1193,28 @@ function loadExperiments() {
// If experiment has finished, load full details, then parse the stdout file
if (exp.experimentStatus === models.ExperimentState.COMPLETED) {
services.FullExperimentService.retrieve({
- lookup: exp.experimentId
+ lookup: exp.experimentId,
})
- .then(fullDetails => {
+ .then((fullDetails) => {
const stdoutDataProductId = fullDetails.experiment.experimentOutputs.find(
- o => o.name === "Echo-STDOUT"
+ (o) => o.name === "Echo-STDOUT"
).value;
const stdoutDataProduct = fullDetails.outputDataProducts.find(
- dp => dp.productUri === stdoutDataProductId
+ (dp) => dp.productUri === stdoutDataProductId
);
if (
stdoutDataProduct &&
stdoutDataProduct.downloadURL
) {
return fetch(stdoutDataProduct.downloadURL, {
- credentials: "same-origin"
- }).then(result => result.text());
+ credentials: "same-origin",
+ }).then((result) => result.text());
} else {
// If we can't download it, fake it
return FAKE_STDOUT;
}
})
- .then(text => {
+ .then((text) => {
$(`#output_${index}`).text(text);
});
}
diff --git a/docs/tutorial/screenshots/gateways19/gaussian-eigenvalues-show_grid.png b/docs/tutorial/screenshots/gateways19/gaussian-eigenvalues-show_grid.png
new file mode 100644
index 0000000..07bb66d
Binary files /dev/null and b/docs/tutorial/screenshots/gateways19/gaussian-eigenvalues-show_grid.png differ
diff --git a/mkdocs.yml b/mkdocs.yml
index 7740248..6bf42ea 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -8,14 +8,15 @@ nav:
- Using the integrated CMS: cms.md
- Developer Guide:
- Adding a Django App: dev/new_django_app.md
- - Adding a Custom Django App: dev/custom_django_app.md
- Developing the Frontend: dev/developing_frontend.md
- Developing the Backend: dev/developing_backend.md
- Developing a Wagtail Export (theme): dev/wagtail_export.md
+ - Customization Guide:
+ - Gateways 2019 Tutorial: tutorial/gateways2019_tutorial.md
+ - Adding a Custom Django App: dev/custom_django_app.md
+ - Adding a Custom Output View Provider: dev/custom_output_view_provider.md
- Administrator Guide:
- Tusd Installation: admin/tusd.md
- - Tutorials:
- - Gateways 2019: tutorial/gateways2019_tutorial.md
theme: readthedocs