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/05/13 15:59:57 UTC

[airavata-django-portal] branch master updated: AIRAVATA-3039 Handle deleted application when viewing/editing experiment

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


The following commit(s) were added to refs/heads/master by this push:
     new 0266b7e  AIRAVATA-3039 Handle deleted application when viewing/editing experiment
0266b7e is described below

commit 0266b7e9d0e0cb091e8b6ed30930bdf86e69c008
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Mon May 13 11:59:08 2019 -0400

    AIRAVATA-3039 Handle deleted application when viewing/editing experiment
    
    Don't allow editing or cloning an experiment where the application
    is no longer available.
---
 .../django_airavata_api/js/models/Experiment.js    |  8 ++
 .../js/models/FullExperiment.js                    |  4 +
 django_airavata/apps/api/views.py                  | 26 +++++--
 .../js/components/experiment/ExperimentSummary.vue | 16 +++-
 .../js/containers/EditExperimentContainer.vue      | 25 +++++-
 .../js/containers/ExperimentListContainer.vue      | 88 +++++++++++++++++-----
 .../js/containers/RecentExperimentsContainer.vue   | 58 ++++++++------
 7 files changed, 166 insertions(+), 59 deletions(-)

diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js b/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js
index 3ff8c35..7052299 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js
@@ -127,6 +127,14 @@ export default class Experiment extends BaseModel {
     );
   }
 
+  get resourceHostId() {
+    return this.userConfigurationData &&
+      this.userConfigurationData.computationalResourceScheduling
+      ? this.userConfigurationData.computationalResourceScheduling
+          .resourceHostId
+      : null;
+  }
+
   populateInputsOutputsFromApplicationInterface(applicationInterface) {
     // Copy application inputs and outputs to the experiment
     this.experimentInputs = applicationInterface.applicationInputs.map(input =>
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 62b8700..60b0cfb 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
@@ -59,6 +59,10 @@ export default class FullExperiment extends BaseModel {
         return this.computeResource ? this.computeResource.hostName : null;
     }
 
+    get resourceHostId() {
+      return this.experiment.resourceHostId;
+    }
+
     get experimentStatus() {
         return this.experiment.latestStatus;
     }
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index 75151a9..989e4ae 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -404,19 +404,29 @@ class FullExperimentViewSet(mixins.RetrieveModelMixin,
             for dp in inp.value.split(',')
             if inp.value.startswith('airavata-dp')]
         appInterfaceId = experimentModel.executionId
-        applicationInterface = self.request.airavata_client \
-            .getApplicationInterface(self.authz_token, appInterfaceId)
-        appModuleId = applicationInterface.applicationModules[0]
-        applicationModule = self.request.airavata_client \
-            .getApplicationModule(self.authz_token, appModuleId)
+        try:
+            applicationInterface = self.request.airavata_client \
+                .getApplicationInterface(self.authz_token, appInterfaceId)
+            appModuleId = applicationInterface.applicationModules[0]
+            applicationModule = self.request.airavata_client \
+                .getApplicationModule(self.authz_token, appModuleId)
+        except Exception as e:
+            log.exception("Failed to load app interface/module")
+            applicationModule = None
+
         compute_resource_id = None
         user_conf = experimentModel.userConfigurationData
         if user_conf and user_conf.computationalResourceScheduling:
             comp_res_sched = user_conf.computationalResourceScheduling
             compute_resource_id = comp_res_sched.resourceHostId
-        compute_resource = self.request.airavata_client.getComputeResource(
-            self.authz_token, compute_resource_id) \
-            if compute_resource_id else None
+        try:
+            compute_resource = self.request.airavata_client.getComputeResource(
+                self.authz_token, compute_resource_id) \
+                if compute_resource_id else None
+        except Exception as e:
+            log.exception("Failed to load compute resource for {}".format(
+                compute_resource_id))
+            compute_resource = None
         try:
             project = self.request.airavata_client.getProject(
                 self.authz_token, experimentModel.projectId)
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 f2003d5..65320e6 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
@@ -8,11 +8,11 @@
       </div>
       <div class="col-auto">
           <share-button :entity-id="experiment.experimentId" />
-          <b-link v-if="experiment.isEditable" class="btn btn-primary" :href="editLink">
+          <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 variant="primary" @click="clone">
+          <b-btn v-if="isClonable" variant="primary" @click="clone">
             Clone
             <i class="fa fa-copy" aria-hidden="true"></i>
           </b-btn>
@@ -77,11 +77,13 @@
                 </tr>
                 <tr>
                   <th scope="row">Application</th>
-                  <td>{{ localFullExperiment.applicationName }}</td>
+                  <td v-if="localFullExperiment.applicationName">{{ localFullExperiment.applicationName }}</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>{{ localFullExperiment.computeHostName }}</td>
+                  <td v-if="localFullExperiment.computeHostName">{{ localFullExperiment.computeHostName }}</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>
@@ -248,6 +250,12 @@ export default {
     },
     editLink() {
       return urls.editExperiment(this.experiment);
+    },
+    isEditable() {
+      return this.experiment.isEditable && this.localFullExperiment.applicationName;
+    },
+    isClonable() {
+      return this.localFullExperiment.applicationName;
     }
   },
   methods: {
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/EditExperimentContainer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/EditExperimentContainer.vue
index 4c9f67b..02afbf2 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/EditExperimentContainer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/EditExperimentContainer.vue
@@ -12,6 +12,7 @@
 
 <script>
 import { services } from "django-airavata-api";
+import { notifications } from "django-airavata-common-ui";
 import ExperimentEditor from "../components/experiment/ExperimentEditor.vue";
 import urls from "../utils/urls";
 
@@ -39,7 +40,7 @@ export default {
     },
     handleSavedAndLaunchedExperiment: function(experiment) {
       // Redirect to experiment view
-      urls.navigateToViewExperiment(experiment, {launching: true});
+      urls.navigateToViewExperiment(experiment, { launching: true });
     }
   },
   computed: {},
@@ -48,9 +49,14 @@ export default {
       .then(experiment => {
         this.experiment = experiment;
         const appInterfaceId = experiment.executionId;
-        return services.ApplicationInterfaceService.retrieve({
-          lookup: appInterfaceId
-        });
+        return services.ApplicationInterfaceService.retrieve(
+          {
+            lookup: appInterfaceId
+          },
+          {
+            ignoreErrors: true
+          }
+        );
       })
       .then(appInterface => {
         const appModuleId = appInterface.applicationModules[0];
@@ -60,6 +66,17 @@ export default {
       })
       .then(appModule => {
         this.appModule = appModule;
+      })
+      .catch(() => {
+        notifications.NotificationList.add(
+          new notifications.Notification({
+            type: "ERROR",
+            message:
+              "Unable to load application interface (" +
+              this.experiment.executionId +
+              ") or module. If it has been deleted then you won't be able to edit this experiment."
+          })
+        );
       });
   }
 };
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/ExperimentListContainer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/ExperimentListContainer.vue
index 3e20399..90170ee 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/ExperimentListContainer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/ExperimentListContainer.vue
@@ -21,9 +21,18 @@
                 </tr>
               </thead>
               <tbody>
-                <tr v-for="experiment in experiments" :key="experiment.experimentId">
-                  <td><b-link :href="viewLink(experiment)">{{experiment.name}}</b-link></td>
-                  <td>{{applicationName(experiment)}}</td>
+                <tr
+                  v-for="experiment in experiments"
+                  :key="experiment.experimentId"
+                >
+                  <td>
+                    <b-link :href="viewLink(experiment)">{{experiment.name}}</b-link>
+                  </td>
+                  <td v-if="applicationName(experiment)">{{applicationName(experiment)}}</td>
+                  <td
+                    v-else
+                    class="font-italic text-muted"
+                  >N/A</td>
                   <td>{{experiment.userName}}</td>
                   <td>
                     <span :title="experiment.creationTime">{{ fromNow(experiment.creationTime) }}</span>
@@ -32,17 +41,40 @@
                     <experiment-status-badge :statusName="experiment.experimentStatus.name" />
                   </td>
                   <td>
-                    <b-link v-if="experiment.isEditable" :href="editLink(experiment)" class="action-link">Edit
-                      <i class="fa fa-edit" aria-hidden="true"></i>
-                    </b-link>
-                    <b-link v-else @click="clone(experiment)" class="action-link">Clone
-                      <i class="fa fa-copy" aria-hidden="true"></i>
-                    </b-link>
+                    <!-- if we can't load the application for the experiment
+                    (for example, if it was deleted), then user can't edit or
+                    clone experiment -->
+                    <span v-if="applicationName(experiment)">
+                      <b-link
+                        v-if="experiment.isEditable && applicationName(experiment)"
+                        :href="editLink(experiment)"
+                        class="action-link"
+                      >Edit
+                        <i
+                          class="fa fa-edit"
+                          aria-hidden="true"
+                        ></i>
+                      </b-link>
+                      <b-link
+                        v-else
+                        @click="clone(experiment)"
+                        class="action-link"
+                      >Clone
+                        <i
+                          class="fa fa-copy"
+                          aria-hidden="true"
+                        ></i>
+                      </b-link>
+                    </span>
                   </td>
                 </tr>
               </tbody>
             </table>
-            <pager v-bind:paginator="experimentsPaginator" v-on:next="nextExperiments" v-on:previous="previousExperiments"></pager>
+            <pager
+              v-bind:paginator="experimentsPaginator"
+              v-on:next="nextExperiments"
+              v-on:previous="previousExperiments"
+            ></pager>
           </div>
         </div>
       </div>
@@ -57,7 +89,7 @@ import { components as comps } from "django-airavata-common-ui";
 import ExperimentStatusBadge from "../components/experiment/ExperimentStatusBadge.vue";
 
 import moment from "moment";
-import urls from '../utils/urls';
+import urls from "../utils/urls";
 
 export default {
   props: ["initialExperimentsData"],
@@ -96,13 +128,31 @@ export default {
         ) {
           return this.applicationInterfaces[experiment.executionId]
             .applicationName;
+        } else if (
+          this.applicationInterfaces[experiment.executionId] === null
+        ) {
+          return null;
         }
       } else {
-        const request = services.ApplicationInterfaceService.retrieve({
-          lookup: experiment.executionId
-        }).then(result =>
-          this.$set(this.applicationInterfaces, experiment.executionId, result)
-        );
+        const request = services.ApplicationInterfaceService.retrieve(
+          {
+            lookup: experiment.executionId
+          },
+          {
+            ignoreErrors: true
+          }
+        )
+          .then(result =>
+            this.$set(
+              this.applicationInterfaces,
+              experiment.executionId,
+              result
+            )
+          )
+          .catch(() => {
+            // Application interface may be deleted
+            this.$set(this.applicationInterfaces, experiment.executionId, null);
+          });
         this.$set(this.applicationInterfaces, experiment.executionId, request);
       }
       return "...";
@@ -123,9 +173,9 @@ export default {
     }
   },
   beforeMount: function() {
-    services.ExperimentSearchService.list({initialData: this.initialExperimentsData}).then(
-      result => (this.experimentsPaginator = result)
-    );
+    services.ExperimentSearchService.list({
+      initialData: this.initialExperimentsData
+    }).then(result => (this.experimentsPaginator = result));
   }
 };
 </script>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/RecentExperimentsContainer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/RecentExperimentsContainer.vue
index 0323fd1..95a6fd9 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/RecentExperimentsContainer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/RecentExperimentsContainer.vue
@@ -31,7 +31,7 @@ export default {
     username: String
   },
   components: {
-    "sidebar": components.Sidebar,
+    sidebar: components.Sidebar,
     "sidebar-header": components.SidebarHeader,
     "sidebar-feed": components.SidebarFeed,
     ExperimentStatusBadge
@@ -41,17 +41,19 @@ export default {
   },
   methods: {
     pollExperiments() {
-      this.loadExperiments().then(() => {
-        setTimeout(
-          function() {
-            this.pollExperiments();
-          }.bind(this),
-          this.refreshDelay
-        );
-      }).catch(() => {
-        // If loading experiments fails, just ignore. This can happen if the
-        // user navigates away from the page while a request is executing.
-      });
+      this.loadExperiments()
+        .then(() => {
+          setTimeout(
+            function() {
+              this.pollExperiments();
+            }.bind(this),
+            this.refreshDelay
+          );
+        })
+        .catch(() => {
+          // If loading experiments fails, just ignore. This can happen if the
+          // user navigates away from the page while a request is executing.
+        });
     },
     loadExperiments() {
       return services.ExperimentSearchService.list(
@@ -74,17 +76,14 @@ export default {
             timestamp: e.statusUpdateTime,
             interfaceId: e.executionId,
             isProgressing: e.convertToExperiment().isProgressing,
-            type:
-              e.executionId in this.applicationInterfaces
-                ? this.applicationInterfaces[e.executionId].applicationName
-                : null
+            type: null
           };
         });
         // Load any application interfaces that haven't been loaded yet, so that
         // we can display the applicationName of each experiment
         const unloadedInterfaceIds = {};
         this.feedItems
-          .filter(i => i.type === null)
+          .filter(i => !(i.interfaceId in this.applicationInterfaces))
           .forEach(i => (unloadedInterfaceIds[i.interfaceId] = true));
         Promise.all(
           Object.keys(unloadedInterfaceIds).map(interfaceId => {
@@ -101,19 +100,30 @@ export default {
           lookup: interfaceId
         },
         {
-          showSpinner: false
+          showSpinner: false,
+          ignoreErrors: true
         }
-      ).then(applicationInterface => {
-        this.applicationInterfaces[interfaceId] = applicationInterface;
-      });
+      )
+        .then(applicationInterface => {
+          this.applicationInterfaces[interfaceId] = applicationInterface;
+        })
+        .catch(() => {
+          // ignore if missing
+          this.applicationInterfaces[interfaceId] = null;
+        });
     },
     populateApplicationNames() {
       this.feedItems
         .filter(i => i.type === null)
         .forEach(feedItem => {
-          feedItem.type = this.applicationInterfaces[
-            feedItem.interfaceId
-          ].applicationName;
+          if (
+            feedItem.interfaceId in this.applicationInterfaces &&
+            this.applicationInterfaces[feedItem.interfaceId]
+          ) {
+            feedItem.type = this.applicationInterfaces[
+              feedItem.interfaceId
+            ].applicationName;
+          }
         });
     }
   },