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/12 19:51:14 UTC

[airavata-django-portal] 01/05: AIRAVATA-3285 InteractiveParametersPanel for all display types

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 202b23bf430877b9132e33616aa48868e6a154d4
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Tue Apr 28 10:09:55 2020 -0400

    AIRAVATA-3285 InteractiveParametersPanel for all display types
---
 django_airavata/apps/api/output_views.py           |  33 +++++-
 django_airavata/apps/api/urls.py                   |   2 +
 django_airavata/apps/api/views.py                  |  37 ++++---
 .../output-displays/DefaultOutputDisplay.vue       |   9 +-
 .../output-displays/HtmlOutputDisplay.vue          |  60 ++++-------
 .../output-displays/ImageOutputDisplay.vue         |  40 ++-----
 .../experiment/output-displays/LinkDisplay.vue     |  26 -----
 .../output-displays/LinkOutputDisplay.vue          |  15 +++
 .../output-displays/OutputDisplayContainer.vue     | 115 +++++++++++++++------
 .../output-displays/OutputViewDataLoader.js        |  36 +++++++
 .../InteractiveParameterCheckboxWidget.vue         |  15 +++
 .../InteractiveParametersPanel.vue                 |  42 ++++++++
 12 files changed, 278 insertions(+), 152 deletions(-)

diff --git a/django_airavata/apps/api/output_views.py b/django_airavata/apps/api/output_views.py
index 0614e56..d10fd25 100644
--- a/django_airavata/apps/api/output_views.py
+++ b/django_airavata/apps/api/output_views.py
@@ -1,3 +1,4 @@
+import inspect
 import json
 import logging
 import os
@@ -158,7 +159,8 @@ def _get_application_output_view_providers(application_interface, output_name):
 def generate_data(request,
                   output_view_provider_id,
                   experiment_output_name,
-                  experiment_id):
+                  experiment_id,
+                  **kwargs):
     output_view_provider = _get_output_view_provider(output_view_provider_id)
     # TODO if output_view_provider is None, return 404
     experiment = request.airavata_client.getExperiment(
@@ -169,16 +171,20 @@ def generate_data(request,
     # TODO: handle experiment_output not found by name
     experiment_output = experiment_output[0]
     # TODO: add experiment_output_dir
+    # convert the extra/interactive arguments to appropriate types
+    kwargs = _convert_params_to_type(output_view_provider, kwargs)
     return _generate_data(request,
                           output_view_provider,
                           experiment_output,
-                          experiment)
+                          experiment,
+                          **kwargs)
 
 
 def _generate_data(request,
                    output_view_provider,
                    experiment_output,
-                   experiment):
+                   experiment,
+                   **kwargs):
     # TODO: handle URI_COLLECTION also
     logger.debug("getting data product for {}".format(experiment_output.value))
     output_file = None
@@ -198,6 +204,23 @@ def _generate_data(request,
             output_file = open(test_output_file, 'rb')
     # TODO: change interface to provide output_file as a path
     # TODO: convert experiment and experiment_output to dict/JSON
-    data = output_view_provider.generate_data(
-        request, experiment_output, experiment, output_file=output_file)
+    data = output_view_provider.generate_data(request,
+                                              experiment_output,
+                                              experiment,
+                                              output_file=output_file,
+                                              **kwargs)
     return data
+
+
+def _convert_params_to_type(output_view_provider, params):
+    method_sig = inspect.signature(output_view_provider.generate_data)
+    method_params = method_sig.parameters
+    for k, v in params.items():
+        if (k in method_params and
+            method_params[k].default is not inspect.Parameter.empty and
+                method_params[k].default is not None):
+            # TODO: handle lists?
+            # Handle boolean and numeric values, converting from string
+            if type(method_params[k]) is not str:
+                params[k] = json.loads(v)
+    return params
diff --git a/django_airavata/apps/api/urls.py b/django_airavata/apps/api/urls.py
index 70db566..af8a0a7 100644
--- a/django_airavata/apps/api/urls.py
+++ b/django_airavata/apps/api/urls.py
@@ -101,6 +101,8 @@ urlpatterns = [
         views.html_output_view, name="html-output"),
     url(r'^image-output',
         views.image_output_view, name="image-output"),
+    url(r'^link-output',
+        views.link_output_view, name="link-output"),
 ]
 
 if logger.isEnabledFor(logging.DEBUG):
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index 174d3b1..6b8ee27 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -1,3 +1,4 @@
+import base64
 import json
 import logging
 import os
@@ -1847,24 +1848,30 @@ def notebook_output_view(request):
 
 
 def html_output_view(request):
-    provider_id = request.GET['provider-id']
-    experiment_id = request.GET['experiment-id']
-    experiment_output_name = request.GET['experiment-output-name']
-    data = output_views.generate_data(request,
-                                      provider_id,
-                                      experiment_output_name,
-                                      experiment_id)
+    data = _generate_output_view_data(request)
     return JsonResponse(data)
 
 
 def image_output_view(request):
-    provider_id = request.GET['provider-id']
-    experiment_id = request.GET['experiment-id']
-    experiment_output_name = request.GET['experiment-output-name']
-    data = output_views.generate_data(request,
-                                      provider_id,
-                                      experiment_output_name,
-                                      experiment_id)
+    data = _generate_output_view_data(request)
     # data should contain 'image' as a file-like object or raw bytes with the
     # file data and 'mime-type' with the images mimetype
-    return HttpResponse(data['image'], content_type=data['mime-type'])
+    data['image'] = base64.b64encode(data['image']).decode('utf-8')
+    return JsonResponse(data)
+
+
+def link_output_view(request):
+    data = _generate_output_view_data(request)
+    return JsonResponse(data)
+
+
+def _generate_output_view_data(request):
+    params = request.GET.copy()
+    provider_id = params.pop('provider-id')[0]
+    experiment_id = params.pop('experiment-id')[0]
+    experiment_output_name = params.pop('experiment-output-name')[0]
+    return output_views.generate_data(request,
+                                      provider_id,
+                                      experiment_output_name,
+                                      experiment_id,
+                                      **params.dict())
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/DefaultOutputDisplay.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/DefaultOutputDisplay.vue
index 6ef5520..f048582 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/DefaultOutputDisplay.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/DefaultOutputDisplay.vue
@@ -1,7 +1,11 @@
 <template>
   <div>
     <div v-for="dp in dataProducts" :key="dp.productUri">
-      <img v-if="dp.isImage && dp.downloadURL" class="image-preview rounded" :src="dp.downloadURL" />
+      <img
+        v-if="dp.isImage && dp.downloadURL"
+        class="image-preview rounded"
+        :src="dp.downloadURL"
+      />
       <data-product-viewer :data-product="dp" :mime-type="fileMimeType" />
     </div>
   </div>
@@ -21,9 +25,6 @@ export default {
     dataProducts: {
       type: Array,
       required: true
-    },
-    data: {
-      type: Object
     }
   },
   components: {
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/HtmlOutputDisplay.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/HtmlOutputDisplay.vue
index 54c6b2e..28cbd1b 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/HtmlOutputDisplay.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/HtmlOutputDisplay.vue
@@ -1,42 +1,31 @@
 <template>
-  <div v-html="rawOutput"/>
+  <div v-html="rawOutput" />
 </template>
 
 <script>
-import { models, utils } from "django-airavata-api";
 export default {
   name: "html-output-display",
   props: {
-    experimentOutput: {
-      type: models.OutputDataObjectType,
-      required: true
-    },
-    dataProducts: {
-      type: Array,
-      required: true
-    },
-    experimentId: {
-      type: String,
-      required: true
-    },
-    providerId: {
-      type: String,
+    viewData: {
+      type: Object,
       required: true
     }
   },
-  data() {
-    return {
-      rawOutput: null,
-      isLoading: true,
-      rawJSFile : null,
-    };
+  computed: {
+    rawOutput() {
+      return this.viewData && this.viewData.output
+        ? this.viewData.output
+        : null;
+    },
+    rawJSFile() {
+      return this.viewData && this.viewData.js ? this.viewData.js : null;
+    }
   },
-  methods : {
+  methods: {
     //Attaches the script to the head, the name of the script can be passed from
     //output view provider
     loadScripts() {
       return new Promise(resolve => {
-
         let scriptEl = document.createElement("script");
         scriptEl.src = this.rawJSFile;
         scriptEl.type = "text/javascript";
@@ -44,32 +33,19 @@ export default {
         // Attach script to head
         document.getElementsByTagName("head")[0].appendChild(scriptEl);
         // Wait for tag to load before promise is resolved
-        scriptEl.addEventListener('load',() => {
+        scriptEl.addEventListener("load", () => {
           resolve();
         });
       });
-    },
-  },
-  created() {
-    utils.FetchUtils.get("/api/html-output", {
-      "experiment-id": this.experimentId,
-      "experiment-output-name": this.experimentOutput.name,
-      "provider-id": this.providerId
-    }).then(data => {
-      this.rawOutput = data.output
-      this.rawJSFile = data.js
-      this.isLoading = false
-    });
+    }
   },
   watch: {
-    isLoading() {
-      if(!this.isLoading) {
+    rawJSFile() {
+      // TODO: check if script is already loaded
+      if (this.rawJSFile) {
         this.loadScripts();
       }
     }
   }
 };
 </script>
-
-<style scoped>
-</style>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/ImageOutputDisplay.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/ImageOutputDisplay.vue
index bb7fc8c..52bf8cf 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/ImageOutputDisplay.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/ImageOutputDisplay.vue
@@ -1,45 +1,23 @@
 <template>
-  <img :src="url" />
+  <img :src="dataUrl" />
 </template>
 
 <script>
-import { models } from "django-airavata-api";
 export default {
   name: "image-output-display",
   props: {
-    experimentOutput: {
-      type: models.OutputDataObjectType,
-      required: true
-    },
-    dataProducts: {
-      type: Array,
-      required: true
-    },
-    experimentId: {
-      type: String,
-      required: true
-    },
-    providerId: {
-      type: String,
+    viewData: {
+      type: Object,
       required: true
     }
   },
-  data() {
-    return {
-      rawOutput: null
-    };
-  },
   computed: {
-    url() {
-      return (
-        "/api/image-output?" +
-        "experiment-id=" +
-        encodeURIComponent(this.experimentId) +
-        "&experiment-output-name=" +
-        encodeURIComponent(this.experimentOutput.name) +
-        "&provider-id=" +
-        encodeURIComponent(this.providerId)
-      );
+    dataUrl() {
+      if (this.viewData) {
+        return `data:${this.viewData["mime-type"]};base64,${this.viewData["image"]}`;
+      } else {
+        return null;
+      }
     }
   }
 };
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/LinkDisplay.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/LinkDisplay.vue
deleted file mode 100644
index 7afd4ca..0000000
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/LinkDisplay.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<template>
-  <a :href="data.url">{{ data.label }}</a>
-</template>
-
-<script>
-import { models } from "django-airavata-api"
-
-export default {
-  name: "link-viewer",
-  props: {
-    experimentOutput: {
-      type: models.OutputDataObjectType,
-      required: true
-    },
-    dataProducts: {
-      type: Array,
-      required: true
-    },
-    data: {
-      type: Object
-    }
-  },
-}
-</script>
-
-
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/LinkOutputDisplay.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/LinkOutputDisplay.vue
new file mode 100644
index 0000000..a2cf4c5
--- /dev/null
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/LinkOutputDisplay.vue
@@ -0,0 +1,15 @@
+<template>
+  <a :href="viewData.url">{{ viewData.label }}</a>
+</template>
+
+<script>
+export default {
+  name: "link-output-display",
+  props: {
+    viewData: {
+      type: Object,
+      required: true
+    }
+  }
+};
+</script>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/OutputDisplayContainer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/OutputDisplayContainer.vue
index 2ae3498..4cd9384 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/OutputDisplayContainer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/OutputDisplayContainer.vue
@@ -1,30 +1,27 @@
 <template>
   <b-card>
-    <div
-      slot="header"
-      class="d-flex align-items-baseline"
-    >
+    <div slot="header" class="d-flex align-items-baseline">
       <h6>{{ experimentOutput.name }}</h6>
-      <b-dropdown
-        v-if="showMenu"
-        :text="currentView['name']"
-        class="ml-auto"
-      >
+      <b-dropdown v-if="showMenu" :text="currentView['name']" class="ml-auto">
         <b-dropdown-item
           v-for="view in outputViews"
           :key="view['provider-id']"
           :active="view['provider-id'] === currentView['provider-id']"
           @click="selectView(view)"
-        >{{ view['name']}}</b-dropdown-item>
+          >{{ view["name"] }}</b-dropdown-item
+        >
       </b-dropdown>
     </div>
     <component
       :is="outputDisplayComponentName"
-      :experiment-output="experimentOutput"
+      :view-data="viewData"
       :data-products="dataProducts"
-      :experiment-id="experimentId"
-      :provider-id="currentView['provider-id']"
-      :data="outputViewData"
+      :experiment-output="experimentOutput"
+    />
+    <interactive-parameters-panel
+      v-if="viewData && viewData.interactive"
+      :parameters="viewData.interactive"
+      @input="parametersUpdated"
     />
   </b-card>
 </template>
@@ -35,8 +32,10 @@ import { components } from "django-airavata-common-ui";
 import DefaultOutputDisplay from "./DefaultOutputDisplay";
 import HtmlOutputDisplay from "./HtmlOutputDisplay";
 import ImageOutputDisplay from "./ImageOutputDisplay";
-import LinkDisplay from "./LinkDisplay";
+import LinkOutputDisplay from "./LinkOutputDisplay";
 import NotebookOutputDisplay from "./NotebookOutputDisplay";
+import InteractiveParametersPanel from "./interactive-parameters/InteractiveParametersPanel";
+import OutputViewDataLoader from "./OutputViewDataLoader";
 
 export default {
   name: "output-viewer-container",
@@ -64,42 +63,100 @@ export default {
     DefaultOutputDisplay,
     HtmlOutputDisplay,
     ImageOutputDisplay,
-    LinkDisplay,
-    NotebookOutputDisplay
+    LinkOutputDisplay,
+    NotebookOutputDisplay,
+    InteractiveParametersPanel
+  },
+  created() {
+    if (this.providerId !== "default") {
+      this.loader = this.createLoader();
+      this.loader.load();
+    }
   },
   data() {
     return {
-      currentView: this.outputViews[0]
+      currentView: this.outputViews[0],
+      loader: null
     };
   },
   computed: {
+    viewData() {
+      return this.loader && this.loader.data
+        ? this.loader.data
+        : this.outputViewData;
+    },
     outputViewData() {
       return this.currentView.data ? this.currentView.data : {};
     },
+    displayTypeData() {
+      return {
+        default: {
+          component: "default-output-display",
+          url: null
+        },
+        link: {
+          component: "link-output-display",
+          url: "/api/link-output/"
+        },
+        notebook: {
+          component: "notebook-output-display",
+          url: "/api/notebook-output/"
+        },
+        html: {
+          component: "html-output-display",
+          url: "/api/html-output/"
+        },
+        image: {
+          component: "image-output-display",
+          url: "/api/image-output/"
+        }
+      };
+    },
+    displayType() {
+      return this.currentView["display-type"];
+    },
     outputDisplayComponentName() {
-      if (this.currentView["display-type"] === "default") {
-        return "default-output-display";
-      } else if (this.currentView["display-type"] === "link") {
-        return "link-display";
-      } else if (this.currentView["display-type"] === "notebook") {
-        return "notebook-output-display";
-      } else if (this.currentView["display-type"] === "html") {
-        return "html-output-display";
-      } else if (this.currentView["display-type"] === "image") {
-        return "image-output-display";
+      if (this.displayType in this.displayTypeData) {
+        return this.displayTypeData[this.displayType].component;
+      } else {
+        return null;
+      }
+    },
+    outputDataURL() {
+      if (this.displayType in this.displayTypeData) {
+        return this.displayTypeData[this.displayType].url;
       } else {
         return null;
       }
     },
     showMenu() {
       return this.outputViews.length > 1;
+    },
+    providerId() {
+      return this.currentView["provider-id"];
     }
   },
   methods: {
     selectView(outputView) {
       this.currentView = outputView;
+      if (this.outputDataURL === null) {
+        this.loader = null;
+      } else {
+        this.loader = this.createLoader();
+        this.loader.load();
+      }
+    },
+    parametersUpdated(newParams) {
+      this.loader.load(newParams);
+    },
+    createLoader() {
+      return new OutputViewDataLoader({
+        url: this.outputDataURL,
+        experimentId: this.experimentId,
+        experimentOutputName: this.experimentOutput.name,
+        providerId: this.providerId
+      });
     }
   }
 };
 </script>
-
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/OutputViewDataLoader.js b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/OutputViewDataLoader.js
new file mode 100644
index 0000000..62a4be7
--- /dev/null
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/OutputViewDataLoader.js
@@ -0,0 +1,36 @@
+import { utils } from "django-airavata-api";
+
+export default class OutputViewDataLoader {
+  constructor({ url, experimentId, experimentOutputName, providerId }) {
+    this.url = url;
+    this.experimentId = experimentId;
+    this.experimentOutputName = experimentOutputName;
+    this.providerId = providerId;
+    this.data = null;
+  }
+
+  load(newParams = null) {
+    if (newParams && this.data) {
+      this.data.interactive = newParams;
+    }
+    return utils.FetchUtils.get(this.url, {
+      "experiment-id": this.experimentId,
+      "experiment-output-name": this.experimentOutputName,
+      "provider-id": this.providerId,
+      ...this.createInteractiveParams()
+    }).then(resp => {
+      this.data = resp;
+      return resp;
+    });
+  }
+
+  createInteractiveParams() {
+    const params = {};
+    if (this.data && this.data.interactive) {
+      this.data.interactive.forEach(p => {
+        params[p.name] = p.value;
+      });
+    }
+    return params;
+  }
+}
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/interactive-parameters/InteractiveParameterCheckboxWidget.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/interactive-parameters/InteractiveParameterCheckboxWidget.vue
new file mode 100644
index 0000000..ab9edf2
--- /dev/null
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/interactive-parameters/InteractiveParameterCheckboxWidget.vue
@@ -0,0 +1,15 @@
+<template>
+  <b-form-checkbox :checked="value" @input="$emit('input', $event)"/>
+</template>
+
+<script>
+export default {
+  name: "interactive-parameter-checkbox-widget",
+  props: {
+    value: {
+      type: Boolean,
+      required: true
+    }
+  }
+};
+</script>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/interactive-parameters/InteractiveParametersPanel.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/interactive-parameters/InteractiveParametersPanel.vue
new file mode 100644
index 0000000..0b3527d
--- /dev/null
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/interactive-parameters/InteractiveParametersPanel.vue
@@ -0,0 +1,42 @@
+<template>
+  <b-card title="Parameters">
+    <b-form-group
+      v-for="param in parameters"
+      :key="param.name"
+      :label="param.name"
+    >
+      <!-- TODO: use dynamic components to pick the right widget for the type of parameter -->
+      <interactive-parameter-checkbox-widget
+        :value="param.value"
+        @input="updated(param, $event)"
+      />
+    </b-form-group>
+  </b-card>
+</template>
+
+<script>
+import InteractiveParameterCheckboxWidget from "./InteractiveParameterCheckboxWidget";
+export default {
+  name: "interactive-parameters-panel",
+  components: {
+    InteractiveParameterCheckboxWidget
+  },
+  props: {
+    parameters: {
+      type: Array,
+      required: true
+    }
+  },
+  methods: {
+    updated(param, value) {
+      const params = this.parametersCopy();
+      const i = params.findIndex(x => x.name === param.name);
+      params[i].value = value;
+      this.$emit("input", params);
+    },
+    parametersCopy() {
+      return JSON.parse(JSON.stringify(this.parameters));
+    }
+  }
+};
+</script>