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 2018/02/15 18:48:43 UTC

[airavata-django-portal] branch master updated (bc99396 -> 61435db)

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

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


    from bc99396  AIRAVATA-2599 Set experimentDataDir
     new d5a47b4  AIRAVATA-2631 Move upload/download to api app
     new aeeb139  AIRAVATA-2631 Add downloadURL to DataProductSerializer
     new f92e2ee  AIRAVATA-2631 Make sure exp dir exists and is world writeable
     new 61435db  AIRAVATA-2631 Download icon

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 django_airavata/apps/api/datastore.py              | 97 ++++++++++++++++++++++
 django_airavata/apps/api/serializers.py            | 10 +++
 .../django_airavata_api/js/models/DataProduct.js   |  1 +
 django_airavata/apps/api/urls.py                   |  4 +-
 django_airavata/apps/api/views.py                  | 58 ++++++++++---
 .../js/components/experiment/ExperimentEditor.vue  |  2 +-
 .../js/components/experiment/ExperimentSummary.vue | 17 +++-
 django_airavata/apps/workspace/urls.py             |  1 -
 django_airavata/apps/workspace/views.py            |  5 +-
 django_airavata/settings.py                        |  2 +
 django_airavata/settings_local.py.sample           |  1 +
 11 files changed, 180 insertions(+), 18 deletions(-)
 create mode 100644 django_airavata/apps/api/datastore.py

-- 
To stop receiving notification emails like this one, please contact
machristie@apache.org.

[airavata-django-portal] 01/04: AIRAVATA-2631 Move upload/download to api app

Posted by ma...@apache.org.
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 d5a47b4d653bcee9ce7c7e865aefca926ebfa76a
Author: Marcus Christie <ma...@iu.edu>
AuthorDate: Thu Feb 15 11:28:01 2018 -0500

    AIRAVATA-2631 Move upload/download to api app
---
 django_airavata/apps/api/datastore.py              | 92 ++++++++++++++++++++++
 django_airavata/apps/api/urls.py                   |  4 +-
 django_airavata/apps/api/views.py                  | 58 +++++++++++---
 .../js/components/experiment/ExperimentEditor.vue  |  2 +-
 .../js/components/experiment/ExperimentSummary.vue |  4 +-
 django_airavata/apps/workspace/urls.py             |  1 -
 django_airavata/apps/workspace/views.py            |  5 +-
 django_airavata/settings_local.py.sample           |  1 +
 8 files changed, 150 insertions(+), 17 deletions(-)

diff --git a/django_airavata/apps/api/datastore.py b/django_airavata/apps/api/datastore.py
new file mode 100644
index 0000000..d029b9f
--- /dev/null
+++ b/django_airavata/apps/api/datastore.py
@@ -0,0 +1,92 @@
+import logging
+import os
+from urllib.parse import urlparse
+
+from django.conf import settings
+from django.core.exceptions import ObjectDoesNotExist, SuspiciousFileOperation
+from django.core.files.storage import FileSystemStorage
+
+from airavata.model.data.replica.ttypes import (DataProductModel,
+                                                DataProductType,
+                                                DataReplicaLocationModel,
+                                                ReplicaLocationCategory,
+                                                ReplicaPersistentType)
+
+experiment_data_storage = FileSystemStorage(
+    location=settings.GATEWAY_DATA_STORE_DIR)
+logger = logging.getLogger(__name__)
+
+
+def exists(data_product):
+    """Check if replica for data product exists in this data store."""
+    filepath = _get_replica_filepath(data_product)
+    try:
+        return experiment_data_storage.exists(filepath) if filepath else False
+    except SuspiciousFileOperation as e:
+        logger.warning("Unable to find file at {} for data product uri {}"
+                       .format(filepath, data_product.productUri))
+        return False
+
+
+def open(data_product):
+    """Open replica for data product if it exists in this data store."""
+    if exists(data_product):
+        filepath = _get_replica_filepath(data_product)
+        return experiment_data_storage.open(filepath)
+    else:
+        raise ObjectDoesNotExist("Replica file does not exist")
+
+
+def save(username, project_name, experiment_name, file):
+    """Save file to username/project name/experiment_name in data store."""
+    exp_dir = os.path.join(
+        experiment_data_storage.get_valid_name(username),
+        experiment_data_storage.get_valid_name(project.name),
+        experiment_data_storage.get_valid_name(experiment_name))
+    file_path = os.path.join(
+        exp_dir,
+        experiment_data_storage.get_valid_name(file.name))
+    input_file_name = experiment_data_storage.save(file_path, file)
+    input_file_fullpath = experiment_data_storage.path(input_file_name)
+    # Create DataProductModel instance with DataReplicaLocationModel
+    data_product = DataProductModel()
+    data_product.gatewayId = settings.GATEWAY_ID
+    data_product.ownerName = username
+    data_product.productName = file.name
+    data_product.dataProductType = DataProductType.FILE
+    data_replica_location = DataReplicaLocationModel()
+    data_replica_location.storageResourceId = \
+        settings.GATEWAY_DATA_STORE_RESOURCE_ID
+    data_replica_location.replicaName = \
+        "{} gateway data store copy".format(file.name)
+    data_replica_location.replicaLocationCategory = \
+        ReplicaLocationCategory.GATEWAY_DATA_STORE
+    data_replica_location.replicaPersistentType = \
+        ReplicaPersistentType.TRANSIENT
+    data_replica_location.filePath = \
+        "file://{}:{}".format(settings.GATEWAY_DATA_STORE_HOSTNAME, input_file_fullpath)
+    data_product.replicaLocations = [data_replica_location]
+    return data_product
+
+
+def get_experiment_dir(username, project_name, experiment_name):
+    experiment_dir = os.path.join(
+        settings.GATEWAY_DATA_STORE_DIR,
+        experiment_data_storage.get_valid_name(username),
+        experiment_data_storage.get_valid_name(project_name),
+        experiment_data_storage.get_valid_name(experiment_name))
+    # TODO: make sure experiment_dir has the appropriate permissions and exists
+    return experiment_dir
+
+
+def _get_replica_filepath(data_product):
+    replica_filepaths = [rep.filePath
+                         for rep in data_product.replicaLocations
+                         if rep.replicaLocationCategory ==
+                         ReplicaLocationCategory.GATEWAY_DATA_STORE]
+    print(replica_filepaths)
+    replica_filepath = (replica_filepaths[0]
+                        if len(replica_filepaths) > 0 else None)
+    if replica_filepath:
+        return urlparse(replica_filepath).path
+    return None
diff --git a/django_airavata/apps/api/urls.py b/django_airavata/apps/api/urls.py
index 4a1e5ad..a9a715a 100644
--- a/django_airavata/apps/api/urls.py
+++ b/django_airavata/apps/api/urls.py
@@ -33,7 +33,9 @@ urlpatterns = [
     url(r'^application/deployment$', views.FetchApplicationDeployment.as_view(), name="app_deployment"),
     url(r'^credentials/ssh/keys', views.FetchSSHPubKeys.as_view(), name="ssh_keys"),
     url(r'^credentials/ssh/key/delete', views.DeleteSSHPubKey.as_view(), name="ssh_key_deletion"),
-    url(r'^credentials/ssh/key/create', views.GenerateRegisterSSHKeys.as_view(), name="ssh_key_creation")
+    url(r'^credentials/ssh/key/create', views.GenerateRegisterSSHKeys.as_view(), name="ssh_key_creation"),
+    url(r'^upload$', views.upload_input_file, name='upload_input_file'),
+    url(r'^download/(?P<data_product_uri>.+)$', views.download_file, name='download_file'),
 ]
 
 
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index 2c58a58..1307012 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -1,4 +1,5 @@
 
+from . import datastore
 from . import serializers
 from . import thrift_utils
 
@@ -16,10 +17,10 @@ from rest_framework.utils.urls import replace_query_param, remove_query_param
 from rest_framework import status
 
 from django.conf import settings
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import ObjectDoesNotExist
 from django.core.files.storage import FileSystemStorage
-from django.http import JsonResponse, Http404
-from django.shortcuts import render
-from django.views.decorators.csrf import csrf_exempt
+from django.http import FileResponse, Http404, JsonResponse
 
 from airavata.model.appcatalog.appdeployment.ttypes import ApplicationModule, ApplicationDeploymentDescription
 from airavata.model.appcatalog.appinterface.ttypes import ApplicationInterfaceDescription
@@ -33,6 +34,7 @@ import os
 
 log = logging.getLogger(__name__)
 
+
 class GenericAPIBackedViewSet(GenericViewSet):
 
     def get_list(self):
@@ -267,16 +269,11 @@ class ExperimentViewSet(APIBackedViewSet):
         experiment.userConfigurationData.storageId =\
             settings.GATEWAY_DATA_STORE_RESOURCE_ID
         # Set the experimentDataDir
-        # TODO: move this to a common code location
         project = self.request.airavata_client.getProject(
             self.authz_token, experiment.projectId)
-        experiment_data_storage = FileSystemStorage(
-            location=settings.GATEWAY_DATA_STORE_DIR)
-        exp_dir = os.path.join(
-            settings.GATEWAY_DATA_STORE_DIR,
-            experiment_data_storage.get_valid_name(self.username),
-            experiment_data_storage.get_valid_name(project.name),
-            experiment_data_storage.get_valid_name(experiment.experimentName))
+        exp_dir = datastore.get_experiment_dir(self.username,
+                                               project.name,
+                                               experiment.experimentName)
         experiment.userConfigurationData.experimentDataDir = exp_dir
         experiment_id = self.request.airavata_client.createExperiment(
             self.authz_token, self.gateway_id, experiment)
@@ -626,3 +623,42 @@ class DeleteSSHPubKey(APIView):
     def post(self, request, format=None):
         gateway_id = settings.GATEWAY_ID
         return Response(request.airavata_client.deleteSSHPubKey(request.authz_token,request.data['token'],gateway_id))
+
+
+@login_required
+def upload_input_file(request):
+    try:
+        username = request.user.username
+        project_id = request.POST['project-id']
+        project = request.airavata_client.getProject(
+            request.authz_token, project_id)
+        exp_name = request.POST['experiment-name']
+        input_file = request.FILES['file']
+        data_product = datastore.save(username, project.name, exp_name,
+                                      input_file)
+        data_product_uri = request.airavata_client.registerDataProduct(
+            request.authz_token, data_product)
+        return JsonResponse({'uploaded': True,
+                             'data-product-uri': data_product_uri})
+    except Exception as e:
+        resp = JsonResponse({'uploaded': False, 'error': str(e)})
+        resp.status_code = 500
+        return resp
+
+
+@login_required
+def download_file(request, data_product_uri):
+    # TODO check that user has access to this file using sharing API
+    data_product = request.airavata_client.getDataProduct(
+        request.authz_token, data_product_uri)
+    if not data_product:
+        raise Http404("data product does not exist")
+    try:
+        data_file = datastore.open(data_product)
+        response = FileResponse(data_file,
+                                content_type="application/octet-stream")
+        response['Content-Disposition'] = ('attachment; filename="{}"'
+                                           .format(data_product.productName))
+        return response
+    except ObjectDoesNotExist as e:
+        raise Http404(str(e)) from e
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
index 954781b..a85cdc8 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
@@ -182,7 +182,7 @@ export default {
                     data.append('file', input.value);
                     data.append('project-id', this.localExperiment.projectId);
                     data.append('experiment-name', this.localExperiment.experimentName);
-                    let uploadRequest = apiUtils.FetchUtils.post('/workspace/upload', data)
+                    let uploadRequest = apiUtils.FetchUtils.post('/api/upload', data)
                         .then(result => input.value = result['data-product-uri'])
                     uploads.push(uploadRequest);
                 }
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 7a327f7..337e53b 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
@@ -29,7 +29,7 @@
                                     <th scope="row">Outputs</th>
                                     <td>
                                         <template v-for="output in localFullExperiment.outputDataProducts">
-                                            {{ output.filename }}
+                                            <a :href="'/api/download/' + encodeURIComponent(output.productUri)">{{ output.filename }}</a>
                                         </template>
                                     </td>
                                 </tr>
@@ -115,7 +115,7 @@
                                     <th scope="row">Inputs</th>
                                     <td>
                                         <template v-for="input in localFullExperiment.inputDataProducts">
-                                            {{ input.filename }}
+                                            <a :href="'/api/download/' + encodeURIComponent(input.productUri)">{{ input.filename }}</a>
                                         </template>
                                     </td>
                                 </tr>
diff --git a/django_airavata/apps/workspace/urls.py b/django_airavata/apps/workspace/urls.py
index b6a0f31..9790ba0 100644
--- a/django_airavata/apps/workspace/urls.py
+++ b/django_airavata/apps/workspace/urls.py
@@ -12,5 +12,4 @@ urlpatterns = [
     url(r'^applications/(?P<app_module_id>[^/]+)/create_experiment$',
         views.create_experiment, name='create_experiment'),
     url(r'^dashboard$', views.dashboard, name='dashboard'),
-    url(r'^upload$', views.upload_input_file, name='upload_input_file'),
 ]
diff --git a/django_airavata/apps/workspace/views.py b/django_airavata/apps/workspace/views.py
index c121a0f..fda96ca 100644
--- a/django_airavata/apps/workspace/views.py
+++ b/django_airavata/apps/workspace/views.py
@@ -6,11 +6,14 @@ from urllib.parse import urlparse
 
 from django.conf import settings
 from django.contrib.auth.decorators import login_required
+from django.core.exceptions import SuspiciousFileOperation
 from django.core.files.storage import FileSystemStorage
-from django.http import JsonResponse
+from django.http import HttpResponse, JsonResponse, FileResponse
+from django.http.response import HttpResponseBadRequest
 from django.shortcuts import render
 from rest_framework.renderers import JSONRenderer
 
+from airavata.model.application.io.ttypes import DataType
 from airavata.model.data.replica.ttypes import (DataProductModel,
                                                 DataProductType,
                                                 DataReplicaLocationModel,
diff --git a/django_airavata/settings_local.py.sample b/django_airavata/settings_local.py.sample
index 1a1a0a2..38c3149 100644
--- a/django_airavata/settings_local.py.sample
+++ b/django_airavata/settings_local.py.sample
@@ -34,6 +34,7 @@ AIRAVATA_API_PORT = 8930
 AIRAVATA_API_SECURE = False
 GATEWAY_DATA_STORE_RESOURCE_ID = '...'
 GATEWAY_DATA_STORE_DIR = '...'
+GATEWAY_DATA_STORE_HOSTNAME = 'localhost'
 
 # Profile Service Configuration
 PROFILE_SERVICE_HOST = AIRAVATA_API_HOST

-- 
To stop receiving notification emails like this one, please contact
machristie@apache.org.

[airavata-django-portal] 03/04: AIRAVATA-2631 Make sure exp dir exists and is world writeable

Posted by ma...@apache.org.
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 f92e2ee2e314491b3dd332c9c3a1009215a28542
Author: Marcus Christie <ma...@iu.edu>
AuthorDate: Thu Feb 15 12:25:44 2018 -0500

    AIRAVATA-2631 Make sure exp dir exists and is world writeable
---
 django_airavata/apps/api/datastore.py | 15 ++++++++++-----
 django_airavata/settings.py           |  2 ++
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/django_airavata/apps/api/datastore.py b/django_airavata/apps/api/datastore.py
index d029b9f..fb09bfa 100644
--- a/django_airavata/apps/api/datastore.py
+++ b/django_airavata/apps/api/datastore.py
@@ -41,7 +41,7 @@ def save(username, project_name, experiment_name, file):
     """Save file to username/project name/experiment_name in data store."""
     exp_dir = os.path.join(
         experiment_data_storage.get_valid_name(username),
-        experiment_data_storage.get_valid_name(project.name),
+        experiment_data_storage.get_valid_name(project_name),
         experiment_data_storage.get_valid_name(experiment_name))
     file_path = os.path.join(
         exp_dir,
@@ -70,12 +70,18 @@ def save(username, project_name, experiment_name, file):
 
 
 def get_experiment_dir(username, project_name, experiment_name):
-    experiment_dir = os.path.join(
-        settings.GATEWAY_DATA_STORE_DIR,
+    """Return an experiment directory (full path) for the given experiment."""
+    experiment_dir_name = os.path.join(
         experiment_data_storage.get_valid_name(username),
         experiment_data_storage.get_valid_name(project_name),
         experiment_data_storage.get_valid_name(experiment_name))
-    # TODO: make sure experiment_dir has the appropriate permissions and exists
+    experiment_dir = experiment_data_storage.path(experiment_dir_name)
+    if not experiment_data_storage.exists(experiment_dir):
+        os.mkdir(experiment_dir,
+                 mode=experiment_data_storage.directory_permissions_mode)
+        # os.mkdir mode isn't always respected so need to chmod to be sure
+        os.chmod(experiment_dir,
+                 mode=experiment_data_storage.directory_permissions_mode)
     return experiment_dir
 
 
@@ -84,7 +90,6 @@ def _get_replica_filepath(data_product):
                          for rep in data_product.replicaLocations
                          if rep.replicaLocationCategory ==
                          ReplicaLocationCategory.GATEWAY_DATA_STORE]
-    print(replica_filepaths)
     replica_filepath = (replica_filepaths[0]
                         if len(replica_filepaths) > 0 else None)
     if replica_filepath:
diff --git a/django_airavata/settings.py b/django_airavata/settings.py
index aae9bd5..fa5ccd9 100644
--- a/django_airavata/settings.py
+++ b/django_airavata/settings.py
@@ -130,6 +130,8 @@ USE_TZ = True
 STATIC_URL = '/static/'
 STATICFILES_DIRS = [os.path.join(BASE_DIR, "django_airavata", "static")]
 
+# Data storage
+FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o777
 
 # Django REST Framework configuration
 REST_FRAMEWORK = {

-- 
To stop receiving notification emails like this one, please contact
machristie@apache.org.

[airavata-django-portal] 02/04: AIRAVATA-2631 Add downloadURL to DataProductSerializer

Posted by ma...@apache.org.
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 aeeb13976a5b2e22f2224cae67115075f8ee6b26
Author: Marcus Christie <ma...@iu.edu>
AuthorDate: Thu Feb 15 11:46:42 2018 -0500

    AIRAVATA-2631 Add downloadURL to DataProductSerializer
---
 django_airavata/apps/api/serializers.py                        | 10 ++++++++++
 .../api/static/django_airavata_api/js/models/DataProduct.js    |  1 +
 .../js/components/experiment/ExperimentSummary.vue             |  7 ++++---
 3 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/django_airavata/apps/api/serializers.py b/django_airavata/apps/api/serializers.py
index 7c3ab02..5a418c8 100644
--- a/django_airavata/apps/api/serializers.py
+++ b/django_airavata/apps/api/serializers.py
@@ -5,6 +5,7 @@ import logging
 from urllib.parse import quote
 
 from django.conf import settings
+from django.urls import reverse
 from rest_framework import serializers
 
 from airavata.model.appcatalog.appdeployment.ttypes import (ApplicationDeploymentDescription,
@@ -25,6 +26,7 @@ from airavata.model.job.ttypes import JobModel
 from airavata.model.status.ttypes import ExperimentStatus
 from airavata.model.workspace.ttypes import Project
 
+from . import datastore
 from . import thrift_utils
 
 log = logging.getLogger(__name__)
@@ -305,6 +307,14 @@ class DataProductSerializer(
     creationTime = UTCPosixTimestampDateTimeField()
     lastModifiedTime = UTCPosixTimestampDateTimeField()
     replicaLocations = DataReplicaLocationSerializer(many=True)
+    downloadURL = serializers.SerializerMethodField()
+
+    def get_downloadURL(self, data_product):
+        """Getter for downloadURL field."""
+        if datastore.exists(data_product):
+            request = self.context['request']
+            return request.build_absolute_uri(reverse('django_airavata_api:download_file', args=[data_product.productUri]))
+        return None
 
 
 # TODO move this into airavata_sdk?
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/DataProduct.js b/django_airavata/apps/api/static/django_airavata_api/js/models/DataProduct.js
index dff0251..1f95ce3 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/DataProduct.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/DataProduct.js
@@ -27,6 +27,7 @@ const FIELDS = [
         type: DataReplicaLocation,
         list: true
     },
+    'downloadURL',
 ];
 
 const FILENAME_REGEX = /[^/]+$/;
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 337e53b..c424d7c 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
@@ -29,7 +29,8 @@
                                     <th scope="row">Outputs</th>
                                     <td>
                                         <template v-for="output in localFullExperiment.outputDataProducts">
-                                            <a :href="'/api/download/' + encodeURIComponent(output.productUri)">{{ output.filename }}</a>
+                                            <span v-if="output.downloadURL"><a :href="output.downloadURL">{{ output.filename }}</a></span>
+                                            <span v-else>{{ output.filename }}</span>
                                         </template>
                                     </td>
                                 </tr>
@@ -111,11 +112,11 @@
                                     <td>{{ experiment.userConfigurationData.computationalResourceScheduling.queueName }}</td>
                                 </tr>
                                 <tr>
-                                    <!-- TODO -->
                                     <th scope="row">Inputs</th>
                                     <td>
                                         <template v-for="input in localFullExperiment.inputDataProducts">
-                                            <a :href="'/api/download/' + encodeURIComponent(input.productUri)">{{ input.filename }}</a>
+                                            <span v-if="input.downloadURL"><a :href="input.downloadURL">{{ input.filename }}</a></span>
+                                            <span v-else>{{ input.filename }}</span>
                                         </template>
                                     </td>
                                 </tr>

-- 
To stop receiving notification emails like this one, please contact
machristie@apache.org.

[airavata-django-portal] 04/04: AIRAVATA-2631 Download icon

Posted by ma...@apache.org.
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 61435db2f12c401a63f273597126119d84fc2d39
Author: Marcus Christie <ma...@iu.edu>
AuthorDate: Thu Feb 15 12:28:57 2018 -0500

    AIRAVATA-2631 Download icon
---
 .../js/components/experiment/ExperimentSummary.vue         | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

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 c424d7c..8e7852b 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
@@ -29,7 +29,12 @@
                                     <th scope="row">Outputs</th>
                                     <td>
                                         <template v-for="output in localFullExperiment.outputDataProducts">
-                                            <span v-if="output.downloadURL"><a :href="output.downloadURL">{{ output.filename }}</a></span>
+                                            <span v-if="output.downloadURL">
+                                                <a :href="output.downloadURL">
+                                                    <i class="fa fa-download"></i>
+                                                    {{ output.filename }}
+                                                </a>
+                                            </span>
                                             <span v-else>{{ output.filename }}</span>
                                         </template>
                                     </td>
@@ -115,7 +120,12 @@
                                     <th scope="row">Inputs</th>
                                     <td>
                                         <template v-for="input in localFullExperiment.inputDataProducts">
-                                            <span v-if="input.downloadURL"><a :href="input.downloadURL">{{ input.filename }}</a></span>
+                                            <span v-if="input.downloadURL">
+                                                <a :href="input.downloadURL">
+                                                    <i class="fa fa-download"></i>
+                                                    {{ input.filename }}
+                                                </a>
+                                            </span>
                                             <span v-else>{{ input.filename }}</span>
                                         </template>
                                     </td>

-- 
To stop receiving notification emails like this one, please contact
machristie@apache.org.