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 2019/06/11 18:30:23 UTC

[airavata-django-portal] 03/03: AIRAVATA-3029 output view provider initial implementation

This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 8e26c80cb6b294cffa89fee35eb9e2a85d3f57c8
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Tue Jun 11 14:29:44 2019 -0400

    AIRAVATA-3029 output view provider initial implementation
---
 django_airavata/apps/api/output_views.py           |  76 +++++
 django_airavata/apps/api/serializers.py            |   4 +-
 .../js/models/FullExperiment.js                    | 113 ++++----
 django_airavata/apps/api/views.py                  |  13 +-
 .../js/components/experiment/ExperimentSummary.vue | 315 ++++++++++++---------
 .../output-displays/DownloadOutputDisplay.vue      |  33 +++
 .../experiment/output-displays/LinkDisplay.vue     |  26 ++
 .../output-displays/OutputDisplayContainer.vue     |  69 +++++
 django_airavata/settings.py                        |   4 +
 9 files changed, 467 insertions(+), 186 deletions(-)

diff --git a/django_airavata/apps/api/output_views.py b/django_airavata/apps/api/output_views.py
new file mode 100644
index 0000000..4d42446
--- /dev/null
+++ b/django_airavata/apps/api/output_views.py
@@ -0,0 +1,76 @@
+import json
+import logging
+
+from django.conf import settings
+
+logger = logging.getLogger(__name__)
+
+
+class DownloadLinkViewProvider:
+    display_type = 'download'
+    immediate = True
+
+    def generate_data(self, experiment_output, experiment, output_file=None):
+        return {
+        }
+
+
+DEFAULT_VIEW_PROVIDERS = {
+    'download': DownloadLinkViewProvider()
+}
+
+
+def get_output_views(experiment):
+    output_views = {}
+    for output in experiment.experimentOutputs:
+        output_views[output.name] = []
+        output_view_provider_names = _get_output_view_providers(output)
+        for output_view_provider_name in output_view_provider_names:
+            output_view_provider = None
+            if output_view_provider_name in DEFAULT_VIEW_PROVIDERS:
+                output_view_provider = DEFAULT_VIEW_PROVIDERS[
+                    output_view_provider_name]
+            elif output_view_provider_name in settings.OUTPUT_VIEW_PROVIDERS:
+                output_view_provider = settings.OUTPUT_VIEWER_PROVIDERS[
+                    output_view_provider_name]
+            else:
+                logger.error("Unable to find output view provider with "
+                             "name '{}'".format(output_view_provider_name))
+            if output_view_provider is not None:
+                if getattr(output_view_provider, 'immediate', False):
+                    # Immediately call generate_data function
+                    # TODO: also pass a file object if URI (and handle
+                    # URI_COLLECTION)
+                    data = output_view_provider.generate_data(
+                        output, experiment)
+                    output_views[output.name].append({
+                        'provider-id': output_view_provider_name,
+                        'display-type': output_view_provider.display_type,
+                        'data': data
+                    })
+                else:
+                    output_views[output.name].append({
+                        'provider-id': output_view_provider_name,
+                        'display-type': output_view_provider.display_type,
+                    })
+    return output_views
+
+
+def _get_output_view_providers(experiment_output):
+    output_view_providers = []
+    logger.debug("experiment_output={}".format(experiment_output))
+    if experiment_output.metaData:
+        try:
+            output_metadata = json.loads(experiment_output.metaData)
+            output_view_providers.extend(
+                output_metadata['output-view-providers'])
+            logger.debug("output_metadata={}".format(output_metadata))
+        except Exception as e:
+            logger.exception(
+                "Failed to parse metadata for output {}".format(
+                    experiment_output.name))
+    if 'download' not in output_view_providers:
+        output_view_providers.insert(0, 'download')
+    # if len(output_view_providers) == 0:
+    #     output_view_providers.extend(_get_default_view_providers())
+    return output_view_providers
diff --git a/django_airavata/apps/api/serializers.py b/django_airavata/apps/api/serializers.py
index 9420cf2..90fd2ed 100644
--- a/django_airavata/apps/api/serializers.py
+++ b/django_airavata/apps/api/serializers.py
@@ -492,7 +492,7 @@ class FullExperiment:
 
     def __init__(self, experimentModel, project=None, outputDataProducts=None,
                  inputDataProducts=None, applicationModule=None,
-                 computeResource=None, jobDetails=None):
+                 computeResource=None, jobDetails=None, outputViews=None):
         self.experiment = experimentModel
         self.experimentId = experimentModel.experimentId
         self.project = project
@@ -501,6 +501,7 @@ class FullExperiment:
         self.applicationModule = applicationModule
         self.computeResource = computeResource
         self.jobDetails = jobDetails
+        self.outputViews = outputViews
 
 
 class JobSerializer(thrift_utils.create_serializer_class(JobModel)):
@@ -519,6 +520,7 @@ class FullExperimentSerializer(serializers.Serializer):
     computeResource = ComputeResourceDescriptionSerializer(read_only=True)
     project = ProjectSerializer(read_only=True)
     jobDetails = JobSerializer(many=True, read_only=True)
+    outputViews = serializers.DictField(read_only=True)
 
     def create(self, validated_data):
         raise Exception("Not implemented")
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/FullExperiment.js b/django_airavata/apps/api/static/django_airavata_api/js/models/FullExperiment.js
index 60b0cfb..f657762 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/FullExperiment.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/FullExperiment.js
@@ -1,63 +1,66 @@
-
-import ApplicationModule from './ApplicationModule'
-import BaseModel from './BaseModel'
-import ComputeResourceDescription from './ComputeResourceDescription'
-import DataProduct from './DataProduct'
-import Experiment from './Experiment'
-import Job from './Job'
-import Project from './Project'
+import ApplicationModule from "./ApplicationModule";
+import BaseModel from "./BaseModel";
+import ComputeResourceDescription from "./ComputeResourceDescription";
+import DataProduct from "./DataProduct";
+import Experiment from "./Experiment";
+import Job from "./Job";
+import Project from "./Project";
 
 const FIELDS = [
-    'experimentId',
-    {
-        name: 'experiment',
-        type: Experiment,
-    },
-    {
-        name: 'project',
-        type: Project
-    },
-    {
-        name: 'applicationModule',
-        type: ApplicationModule
-    },
-    {
-        name: 'computeResource',
-        type: ComputeResourceDescription
-    },
-    {
-        name: 'outputDataProducts',
-        type: DataProduct,
-        list: true
-    },
-    {
-        name: 'inputDataProducts',
-        type: DataProduct,
-        list: true
-    },
-    {
-        name: 'jobDetails',
-        type: Job,
-        list: true,
-    },
+  "experimentId",
+  {
+    name: "experiment",
+    type: Experiment
+  },
+  {
+    name: "project",
+    type: Project
+  },
+  {
+    name: "applicationModule",
+    type: ApplicationModule
+  },
+  {
+    name: "computeResource",
+    type: ComputeResourceDescription
+  },
+  {
+    name: "outputDataProducts",
+    type: DataProduct,
+    list: true
+  },
+  {
+    name: "inputDataProducts",
+    type: DataProduct,
+    list: true
+  },
+  {
+    name: "jobDetails",
+    type: Job,
+    list: true
+  },
+  {
+    name: "outputViews",
+    type: Object
+  }
 ];
 
 export default class FullExperiment extends BaseModel {
-    constructor(data = {}) {
-        super(FIELDS, data);
-    }
+  constructor(data = {}) {
+    super(FIELDS, data);
+  }
 
-    get projectName() {
-        return this.project ? this.project.name : null;
-    }
+  get projectName() {
+    return this.project ? this.project.name : null;
+  }
 
-    get applicationName() {
-        return this.applicationModule ? this.applicationModule.appModuleName : null;
-    }
+  get applicationName() {
+    return this.applicationModule ? this.applicationModule.appModuleName : null;
+  }
 
-    get computeHostName() {
-        return this.computeResource ? this.computeResource.hostName : null;
-    }
+  get computeHostName() {
+    return this.computeResource ? this.computeResource.hostName : null;
+  }
 
     get resourceHostId() {
       return this.experiment.resourceHostId;
@@ -67,7 +70,7 @@ export default class FullExperiment extends BaseModel {
         return this.experiment.latestStatus;
     }
 
-    get experimentStatusName() {
-        return this.experimentStatus ? this.experimentStatus.state.name : null;
-    }
+  get experimentStatusName() {
+    return this.experimentStatus ? this.experimentStatus.state.name : null;
+  }
 }
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index 7d8caf7..0fe5192 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -40,7 +40,14 @@ from django_airavata.apps.api.view_utils import (
 )
 from django_airavata.apps.auth import iam_admin_client
 
-from . import data_products_helper, helpers, models, serializers, thrift_utils
+from . import (
+    data_products_helper,
+    helpers,
+    models,
+    output_views,
+    serializers,
+    thrift_utils
+)
 
 READ_PERMISSION_TYPE = '{}:READ'
 
@@ -419,6 +426,7 @@ class FullExperimentViewSet(mixins.RetrieveModelMixin,
                 output.type == DataType.URI_COLLECTION)
             for dp in output.value.split(',')
             if output.value.startswith('airavata-dp')]
+        exp_output_views = output_views.get_output_views(experimentModel)
         inputDataProducts = [
             self.request.airavata_client.getDataProduct(self.authz_token,
                                                         inp.value)
@@ -474,7 +482,8 @@ class FullExperimentViewSet(mixins.RetrieveModelMixin,
             inputDataProducts=inputDataProducts,
             applicationModule=applicationModule,
             computeResource=compute_resource,
-            jobDetails=job_details)
+            jobDetails=job_details,
+            outputViews=exp_output_views)
         return full_experiment
 
 
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue
index 424afb6..0339172 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue
@@ -7,15 +7,55 @@
         </h1>
       </div>
       <div class="col-auto">
-          <share-button :entity-id="experiment.experimentId" />
-          <b-link v-if="isEditable" class="btn btn-primary" :href="editLink">
-            Edit
-            <i class="fa fa-edit" aria-hidden="true"></i>
-          </b-link>
-          <b-btn v-if="isClonable" variant="primary" @click="clone">
-            Clone
-            <i class="fa fa-copy" aria-hidden="true"></i>
-          </b-btn>
+        <share-button :entity-id="experiment.experimentId" />
+        <b-link
+          v-if="isEditable"
+          class="btn btn-primary"
+          :href="editLink"
+        >
+          Edit
+          <i
+            class="fa fa-edit"
+            aria-hidden="true"
+          ></i>
+        </b-link>
+        <b-btn
+          v-if="isClonable"
+          variant="primary"
+          @click="clone"
+        >
+          Clone
+          <i
+            class="fa fa-copy"
+            aria-hidden="true"
+          ></i>
+        </b-btn>
+      </div>
+    </div>
+    <template v-for="output in experiment.experimentOutputs">
+
+      <div
+        class="row"
+        v-if="outputDataProducts[output.name].length > 0"
+        :key="output.name"
+      >
+        <div class="col">
+          <output-display-container
+            :experiment-output="output"
+            :data-products="outputDataProducts[output.name]"
+            :output-views="localFullExperiment.outputViews[output.name]"
+          />
+        </div>
+      </div>
+    </template>
+    <div class="row">
+      <div class="col">
+        <b-card header="Other Files">
+          <b-link
+            v-if="storageDirLink"
+            :href="storageDirLink"
+          >Storage Directory</b-link>
+        </b-card>
       </div>
     </div>
     <div class="row">
@@ -29,12 +69,15 @@
                   <td>
                     <div :title="experiment.experimentId">{{ experiment.experimentName }}</div>
                     <small class="text-muted">
-                    ID: {{ experiment.experimentId }}
-                    (<clipboard-copy-link :text="experiment.experimentId" :link-classes="['text-reset']">
-                      copy
-                      <span slot="icon"></span>
-                      <span slot="tooltip">Copied ID!</span>
-                    </clipboard-copy-link>)
+                      ID: {{ experiment.experimentId }}
+                      (<clipboard-copy-link
+                        :text="experiment.experimentId"
+                        :link-classes="['text-reset']"
+                      >
+                        copy
+                        <span slot="icon"></span>
+                        <span slot="tooltip">Copied ID!</span>
+                      </clipboard-copy-link>)
                     </small>
                   </td>
                 </tr>
@@ -50,41 +93,24 @@
                   </td>
                 </tr>
                 <tr>
-                  <th scope="row">Outputs</th>
-                  <td>
-                    <ul>
-                      <li v-for="output in experiment.experimentOutputs" :key="output.name">
-                        {{ output.name }}:
-                        <template v-if="output.type.isSimpleValueType">
-                          {{ output.value }}
-                        </template>
-                        <template v-else-if="output.type.isFileValueType">
-                          <data-product-viewer v-for="dp in outputDataProducts[output.name]"
-                            :data-product="dp" class="data-product" :key="dp.productUri"/>
-                        </template>
-                      </li>
-                    </ul>
-                    <b-link v-if="storageDirLink" :href="storageDirLink">Storage Directory</b-link>
-                  </td>
-                </tr>
-                <!-- Going to leave this out for now -->
-                <!-- <tr>
-                                    <th scope="row">Storage Directory</th>
-                                    <td></td>
-                                </tr> -->
-                <tr>
                   <th scope="row">Owner</th>
                   <td>{{ experiment.userName }}</td>
                 </tr>
                 <tr>
                   <th scope="row">Application</th>
                   <td v-if="localFullExperiment.applicationName">{{ localFullExperiment.applicationName }}</td>
-                  <td v-else class="font-italic text-muted">Unable to load interface {{ localFullExperiment.experiment.executionId }}</td>
+                  <td
+                    v-else
+                    class="font-italic text-muted"
+                  >Unable to load interface {{ localFullExperiment.experiment.executionId }}</td>
                 </tr>
                 <tr>
                   <th scope="row">Compute Resource</th>
                   <td v-if="localFullExperiment.computeHostName">{{ localFullExperiment.computeHostName }}</td>
-                  <td v-else class="font-italic text-muted">Unable to load compute resource {{ localFullExperiment.resourceHostId }}</td>
+                  <td
+                    v-else
+                    class="font-italic text-muted"
+                  >Unable to load compute resource {{ localFullExperiment.resourceHostId }}</td>
                 </tr>
                 <tr>
                   <th scope="row">Experiment Status</th>
@@ -101,79 +127,90 @@
                   <td>
                     <table class="table">
                       <thead>
-                        <th>Name</th>
-                        <th>ID</th>
-                        <th>Status</th>
-                        <th>Creation Time</th>
-                      </thead>
-                      <tr v-for="(jobDetail, index) in localFullExperiment.jobDetails" :key="jobDetail.jobId">
-                        <td>{{ jobDetail.jobName }}</td>
-                        <td>{{ jobDetail.jobId }}</td>
-                        <td>{{ jobDetail.jobStatusStateName }}</td>
-                        <td>
-                          <span :title="jobDetail.creationTime.toString()">{{ jobCreationTimes[index] }}</span>
-                        </td>
-                      </tr>
-                    </table>
+                  <th>Name</th>
+                  <th>ID</th>
+                  <th>Status</th>
+                  <th>Creation Time</th>
+                  </thead>
+                <tr
+                  v-for="(jobDetail, index) in localFullExperiment.jobDetails"
+                  :key="jobDetail.jobId"
+                >
+                  <td>{{ jobDetail.jobName }}</td>
+                  <td>{{ jobDetail.jobId }}</td>
+                  <td>{{ jobDetail.jobStatusStateName }}</td>
+                  <td>
+                    <span :title="jobDetail.creationTime.toString()">{{ jobCreationTimes[index] }}</span>
                   </td>
                 </tr>
-                <!--  TODO: leave this out for now -->
-                <!-- <tr>
+            </table>
+            </td>
+            </tr>
+            <!--  TODO: leave this out for now -->
+            <!-- <tr>
                                     <th scope="row">Notification List</th>
                                     <td>{{ experiment.emailAddresses
                                             ? experiment.emailAddresses.join(", ")
                                             : '' }}</td>
                                 </tr> -->
-                <tr>
-                  <th scope="row">Creation Time</th>
-                  <td>
-                    <span :title="experiment.creationTime.toString()">{{ creationTime }}</span>
-                  </td>
-                </tr>
-                <tr>
-                  <th scope="row">Last Modified Time</th>
-                  <td>
-                    <span :title="localFullExperiment.experimentStatus.timeOfStateChange.toString()">{{ lastModifiedTime }}</span>
-                  </td>
-                </tr>
-                <tr>
-                  <th scope="row">Wall Time Limit</th>
-                  <td>{{ experiment.userConfigurationData.computationalResourceScheduling.wallTimeLimit }} minutes</td>
-                </tr>
-                <tr>
-                  <th scope="row">CPU Count</th>
-                  <td>{{ experiment.userConfigurationData.computationalResourceScheduling.totalCPUCount }}</td>
-                </tr>
-                <tr>
-                  <th scope="row">Node Count</th>
-                  <td>{{ experiment.userConfigurationData.computationalResourceScheduling.nodeCount }}</td>
-                </tr>
-                <tr>
-                  <th scope="row">Queue</th>
-                  <td>{{ experiment.userConfigurationData.computationalResourceScheduling.queueName }}</td>
-                </tr>
-                <tr>
-                  <th scope="row">Inputs</th>
-                  <td>
-                    <ul>
-                      <li v-for="input in experiment.experimentInputs" :key="input.name">
-                        {{ input.name }}:
-                        <template v-if="input.type.isSimpleValueType">
-                          <span class="text-break">{{ input.value }}</span>
-                        </template>
-                        <data-product-viewer v-for="dp in inputDataProducts[input.name]"
-                          v-else-if="input.type.isFileValueType"
-                          :data-product="dp" :input-file="true" class="data-product" :key="dp.productUri"/>
-                      </li>
-                    </ul>
-                  </td>
-                </tr>
-                <tr>
-                  <!-- TODO -->
-                  <th scope="row">Errors</th>
-                  <td></td>
-                </tr>
-              </tbody>
+            <tr>
+              <th scope="row">Creation Time</th>
+              <td>
+                <span :title="experiment.creationTime.toString()">{{ creationTime }}</span>
+              </td>
+            </tr>
+            <tr>
+              <th scope="row">Last Modified Time</th>
+              <td>
+                <span :title="localFullExperiment.experimentStatus.timeOfStateChange.toString()">{{ lastModifiedTime }}</span>
+              </td>
+            </tr>
+            <tr>
+              <th scope="row">Wall Time Limit</th>
+              <td>{{ experiment.userConfigurationData.computationalResourceScheduling.wallTimeLimit }} minutes</td>
+            </tr>
+            <tr>
+              <th scope="row">CPU Count</th>
+              <td>{{ experiment.userConfigurationData.computationalResourceScheduling.totalCPUCount }}</td>
+            </tr>
+            <tr>
+              <th scope="row">Node Count</th>
+              <td>{{ experiment.userConfigurationData.computationalResourceScheduling.nodeCount }}</td>
+            </tr>
+            <tr>
+              <th scope="row">Queue</th>
+              <td>{{ experiment.userConfigurationData.computationalResourceScheduling.queueName }}</td>
+            </tr>
+            <tr>
+              <th scope="row">Inputs</th>
+              <td>
+                <ul>
+                  <li
+                    v-for="input in experiment.experimentInputs"
+                    :key="input.name"
+                  >
+                    {{ input.name }}:
+                    <template v-if="input.type.isSimpleValueType">
+                      <span class="text-break">{{ input.value }}</span>
+                    </template>
+                    <data-product-viewer
+                      v-for="dp in inputDataProducts[input.name]"
+                      v-else-if="input.type.isFileValueType"
+                      :data-product="dp"
+                      :input-file="true"
+                      class="data-product"
+                      :key="dp.productUri"
+                    />
+                  </li>
+                </ul>
+              </td>
+            </tr>
+            <tr>
+              <!-- TODO -->
+              <th scope="row">Errors</th>
+              <td></td>
+            </tr>
+            </tbody>
             </table>
           </div>
         </div>
@@ -186,6 +223,7 @@
 import { models, services } from "django-airavata-api";
 import { components } from "django-airavata-common-ui";
 import DataProductViewer from "./DataProductViewer.vue";
+import OutputDisplayContainer from "./output-displays/OutputDisplayContainer";
 import urls from "../../utils/urls";
 
 import moment from "moment";
@@ -203,33 +241,46 @@ export default {
     }
   },
   data() {
-
     return {
       localFullExperiment: this.fullExperiment.clone()
     };
-
   },
   components: {
     DataProductViewer,
     "clipboard-copy-link": components.ClipboardCopyLink,
-    "share-button": components.ShareButton
+    "share-button": components.ShareButton,
+    OutputDisplayContainer
   },
   computed: {
     inputDataProducts() {
       const result = {};
-      if (this.localFullExperiment && this.localFullExperiment.inputDataProducts) {
+      if (
+        this.localFullExperiment &&
+        this.localFullExperiment.inputDataProducts
+      ) {
         this.localFullExperiment.experiment.experimentInputs.forEach(input => {
-          result[input.name] = this.getDataProducts(input, this.localFullExperiment.inputDataProducts);
+          result[input.name] = this.getDataProducts(
+            input,
+            this.localFullExperiment.inputDataProducts
+          );
         });
       }
       return result;
     },
     outputDataProducts() {
       const result = {};
-      if (this.localFullExperiment && this.localFullExperiment.outputDataProducts) {
-        this.localFullExperiment.experiment.experimentOutputs.forEach(output => {
-          result[output.name] = this.getDataProducts(output, this.localFullExperiment.outputDataProducts);
-        });
+      if (
+        this.localFullExperiment &&
+        this.localFullExperiment.outputDataProducts
+      ) {
+        this.localFullExperiment.experiment.experimentOutputs.forEach(
+          output => {
+            result[output.name] = this.getDataProducts(
+              output,
+              this.localFullExperiment.outputDataProducts
+            );
+          }
+        );
       }
       return result;
     },
@@ -253,14 +304,16 @@ export default {
       return urls.editExperiment(this.experiment);
     },
     isEditable() {
-      return this.experiment.isEditable && this.localFullExperiment.applicationName;
+      return (
+        this.experiment.isEditable && this.localFullExperiment.applicationName
+      );
     },
     isClonable() {
       return this.localFullExperiment.applicationName;
     },
     storageDirLink() {
       if (this.experiment.relativeExperimentDataDir) {
-        return urls.storageDirectory(this.experiment.relativeExperimentDataDir)
+        return urls.storageDirectory(this.experiment.relativeExperimentDataDir);
       } else {
         return null;
       }
@@ -280,12 +333,14 @@ export default {
             !this.localFullExperiment.experiment.hasLaunched) ||
           this.localFullExperiment.experiment.isProgressing
         ) {
-          this.loadExperiment().then(() => {
-            setTimeout(pollExperiment.bind(this), 3000);
-          }).catch(() => {
-            // Wait 30 seconds after an error and then try again
-            setTimeout(pollExperiment.bind(this), 30000);
-          });
+          this.loadExperiment()
+            .then(() => {
+              setTimeout(pollExperiment.bind(this), 3000);
+            })
+            .catch(() => {
+              // Wait 30 seconds after an error and then try again
+              setTimeout(pollExperiment.bind(this), 30000);
+            });
         }
       }.bind(this);
       setTimeout(pollExperiment, 3000);
@@ -295,7 +350,7 @@ export default {
         lookup: this.experiment.experimentId
       }).then(clonedExperiment => {
         urls.navigateToEditExperiment(clonedExperiment);
-      })
+      });
     },
     getDataProducts(io, collection) {
       if (!io.value || !collection) {
@@ -303,13 +358,17 @@ export default {
       }
       let dataProducts = null;
       if (io.type === models.DataType.URI_COLLECTION) {
-        const dataProductURIs = io.value.split(',');
-        dataProducts = dataProductURIs.map(uri => collection.find(dp => dp.productUri === uri));
+        const dataProductURIs = io.value.split(",");
+        dataProducts = dataProductURIs.map(uri =>
+          collection.find(dp => dp.productUri === uri)
+        );
       } else {
         const dataProductURI = io.value;
-        dataProducts = collection.filter(dp => dp.productUri === dataProductURI);
+        dataProducts = collection.filter(
+          dp => dp.productUri === dataProductURI
+        );
       }
-      return dataProducts ? dataProducts.filter(dp => dp ? true : false) : [];
+      return dataProducts ? dataProducts.filter(dp => (dp ? true : false)) : [];
     }
   },
   watch: {},
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/DownloadOutputDisplay.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/DownloadOutputDisplay.vue
new file mode 100644
index 0000000..51657c5
--- /dev/null
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/DownloadOutputDisplay.vue
@@ -0,0 +1,33 @@
+<template>
+  <div>
+    <data-product-viewer v-for="dp in dataProducts"
+      :data-product="dp" class="data-product" :key="dp.productUri"/>
+  </div>
+</template>
+
+<script>
+import { models } from "django-airavata-api"
+import DataProductViewer from "../DataProductViewer.vue";
+
+export default {
+  name: "download-output-viewer",
+  props: {
+    experimentOutput: {
+      type: models.OutputDataObjectType,
+      required: true
+    },
+    dataProducts: {
+      type: Array,
+      required: true
+    },
+    data: {
+      type: Object
+    }
+  },
+  components: {
+    DataProductViewer
+  }
+}
+</script>
+
+
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
new file mode 100644
index 0000000..7afd4ca
--- /dev/null
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/LinkDisplay.vue
@@ -0,0 +1,26 @@
+<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/OutputDisplayContainer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/OutputDisplayContainer.vue
new file mode 100644
index 0000000..3b4b2ea
--- /dev/null
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/OutputDisplayContainer.vue
@@ -0,0 +1,69 @@
+<template>
+  <!-- TODO: Add menu when there are more than one outputViews -->
+  <b-card :title="experimentOutput.name">
+    <component
+      :is="outputDisplayComponentName"
+      :experiment-output="experimentOutput"
+      :data-products="dataProducts"
+      :data="outputViewData"
+    />
+  </b-card>
+</template>
+
+<script>
+import { models } from "django-airavata-api";
+import DownloadOutputDisplay from "./DownloadOutputDisplay";
+import LinkDisplay from "./LinkDisplay";
+import DataProductViewer from "../DataProductViewer";
+
+export default {
+  name: "output-viewer-container",
+  props: {
+    experimentOutput: {
+      type: models.OutputDataObjectType,
+      required: true
+    },
+    outputViews: {
+      type: Array,
+      required: true
+    },
+    dataProducts: {
+      type: Array,
+      required: false,
+      default: null
+    }
+  },
+  components: {
+    DataProductViewer,
+    DownloadOutputDisplay,
+    LinkDisplay
+  },
+  computed: {
+    // TODO: support multiple output views
+    outputViewData() {
+      return this.outputView ? this.outputView["data"] : null;
+    },
+    outputDisplayComponentName() {
+      if (this.outputView) {
+        if (this.outputView["display-type"] === "download") {
+          return "download-output-display";
+        } else if (this.outputView["display-type"] === "link") {
+          return "link-display";
+        } else {
+          return null;
+        }
+      } else {
+        return null;
+      }
+    },
+    outputView() {
+      if (this.outputViews && this.outputViews.length > 0) {
+        return this.outputViews[0];
+      } else {
+        return null;
+      }
+    }
+  }
+};
+</script>
+
diff --git a/django_airavata/settings.py b/django_airavata/settings.py
index 041b594..1c528c6 100644
--- a/django_airavata/settings.py
+++ b/django_airavata/settings.py
@@ -100,6 +100,10 @@ for entry_point in iter_entry_points(group='airavata.djangoapp'):
     INSTALLED_APPS.append("{}.{}".format(entry_point.module_name,
                                          entry_point.attrs[0]))
 
+OUTPUT_VIEW_PROVIDERS = {}
+for entry_point in iter_entry_points(group='airavata.output_view_providers'):
+    OUTPUT_VIEW_PROVIDERS[entry_point.name] = entry_point.load()
+
 MIDDLEWARE = [
     'django.middleware.security.SecurityMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',