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',